Skip to content

Commit

Permalink
feat(ForgeLayout): add modeswitcher, breadcrumb navigation, and … (#1550
Browse files Browse the repository at this point in the history
)

## 📝 Changes

- Adds control components for ForgeLayout
  [x] `ForgeLayout.ModeSwitcher`
  [x] `ForgeLayout.BreadcrumbsNavigation`
  [x] `ForgeLayout.BackButton`
  [x] `ForgeLayout.Breadcrumbs`
  [x] `ForgeLayout.Breadcrumb`

## ✅ Checklist

Easy UI has certain UX standards that must be met. In general,
non-trivial changes should meet the following criteria:

- [x] Visuals match Design Specs in Figma
- [x] Stories accompany any component changes
- [x] Code is in accordance with our style guide
- [x] Design tokens are utilized
- [x] Unit tests accompany any component changes
- [x] TSDoc is written for any API surface area
- [x] Specs are up-to-date
- [x] Console is free from warnings
- [x] No accessibility violations are reported
- [x] Cross-browser check is performed (Chrome, Safari, Firefox)
- [x] Changeset is added

~Strikethrough~ any items that are not applicable to this pull request.
  • Loading branch information
OskiTheCoder authored Dec 23, 2024
1 parent bf0defe commit 462b0a4
Show file tree
Hide file tree
Showing 9 changed files with 518 additions and 87 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-seas-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@easypost/easy-ui": minor
---

feat(ForgeLayout): add modeswitcher, breadcrumb navigation, and search
34 changes: 0 additions & 34 deletions easy-ui-react/src/ForgeLayout/ForgeLayout.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,41 +61,7 @@
flex-direction: column;
}

.header {
position: sticky;
top: 0;
height: component-token("forge-layout", "header-height");

display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
min-width: 0;

border-bottom: 2px solid transparent;
z-index: design-token("z-index.nav");
}

.headerBg {
position: absolute;
top: 0;
left: -100vw;
width: 300vw;
height: component-token("forge-layout", "header-height");

background-color: design-token("color.neutral.025");
border-bottom: component-token("forge-layout", "header-border-width") solid
component-token("forge-layout", "header-border-color");
}

.content {
padding-top: component-token("forge-layout", "shell-gutter");
padding-bottom: component-token("forge-layout", "shell-gutter");
}

.controls {
position: relative;
display: flex;
align-items: center;
gap: design-token("space.2");
}
17 changes: 14 additions & 3 deletions easy-ui-react/src/ForgeLayout/ForgeLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,21 @@ const Template = (args: Partial<ForgeLayoutProps>) => {
<ForgeLayout.Body>
<ForgeLayout.Header>
<ForgeLayout.Controls visibleWhenNavStateIs="collapsed">
<div>Controls when collapsed</div>
<ForgeLayout.BreadcrumbsNavigation>
<ForgeLayout.BackButton onPress={action("Going back!")}>
Back
</ForgeLayout.BackButton>
<ForgeLayout.Breadcrumbs>
<ForgeLayout.Breadcrumb>Sub Account</ForgeLayout.Breadcrumb>
<ForgeLayout.Breadcrumb>
Sub Account Name
</ForgeLayout.Breadcrumb>
</ForgeLayout.Breadcrumbs>
</ForgeLayout.BreadcrumbsNavigation>
</ForgeLayout.Controls>
<ForgeLayout.Controls visibleWhenNavStateIs="expanded">
<div>Controls when expanded</div>
<ForgeLayout.ModeSwitcher onModeChange={action("Mode changed!")} />
<ForgeLayout.Search />
</ForgeLayout.Controls>
<ForgeLayout.Actions>
<ForgeLayout.MenuAction
Expand Down Expand Up @@ -106,7 +117,7 @@ const Template = (args: Partial<ForgeLayoutProps>) => {
</ForgeLayout.Header>
<ForgeLayout.Content>
<Card background="primary" boxShadow="1" variant="solid">
<div style={{ height: 4000 }}>Page Content</div>
<div style={{ height: 1000 }}>Page Content</div>
</Card>
</ForgeLayout.Content>
</ForgeLayout.Body>
Expand Down
50 changes: 44 additions & 6 deletions easy-ui-react/src/ForgeLayout/ForgeLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ describe("<ForgeLayout />", () => {

it("should render a forge layout", async () => {
const handleMenuAction1 = vi.fn();
const handleModeChange = vi.fn();

const { user } = render(
createForgeLayout({
selectedHref: "/1",
onMenuAction1: handleMenuAction1,
onModeChange: handleModeChange,
}),
);

Expand All @@ -58,22 +60,44 @@ describe("<ForgeLayout />", () => {
);

expect(handleMenuAction1).toBeCalled();

expect(
screen.getByRole("button", { name: "Production" }),
).toBeInTheDocument();
expect(
screen.getByRole("searchbox", { name: "Search for content" }),
).toBeInTheDocument();

await userClick(user, screen.getByRole("button", { name: "Production" }));
const radios = screen.getAllByRole("radio");
expect(radios[0]).not.toBeChecked();
expect(radios[1]).toBeChecked();
await userClick(user, radios[0]);

expect(handleModeChange).toBeCalled();
});

it("should render collapsed state", async () => {
render(
const handleBackButton = vi.fn();
const { user } = render(
createForgeLayout({
navState: "collapsed",
selectedHref: "/1",
onBackButton: handleBackButton,
}),
);
expect(screen.getByRole("button", { name: "Back" })).toBeInTheDocument();
expect(screen.queryByText("Breadcrumb One")).toBeInTheDocument();
expect(screen.queryByText("Breadcrumb Two")).toBeInTheDocument();
expect(screen.queryByText("Breadcrumb Three")).toBeInTheDocument();
expect(
screen.queryByRole("navigation", { name: "Main" }),
screen.queryByRole("button", { name: "Production" }),
).not.toBeInTheDocument();
expect(
screen.queryByText("Controls when expanded"),
screen.queryByRole("searchbox", { name: "Search for content" }),
).not.toBeInTheDocument();
expect(screen.queryByText("Controls when collapsed")).toBeInTheDocument();
await userClick(user, screen.getByRole("button", { name: "Back" }));
expect(handleBackButton).toBeCalled();
});

it("should render test mode", async () => {
Expand All @@ -98,6 +122,8 @@ function createForgeLayout(
selectedHref?: string;
onMenuAction1?: () => void;
onMenuAction2?: () => void;
onBackButton?: () => void;
onModeChange?: () => void;
} = {},
) {
const {
Expand All @@ -107,6 +133,8 @@ function createForgeLayout(
selectedHref = "/1",
onMenuAction1 = vi.fn(),
onMenuAction2 = vi.fn(),
onBackButton = vi.fn(),
onModeChange = vi.fn(),
} = props;
return (
<ForgeLayout mode={mode} navState={navState}>
Expand All @@ -125,10 +153,20 @@ function createForgeLayout(
</ForgeLayout.Nav>
<ForgeLayout.Header>
<ForgeLayout.Controls visibleWhenNavStateIs="collapsed">
<div>Controls when collapsed</div>
<ForgeLayout.BreadcrumbsNavigation>
<ForgeLayout.BackButton onPress={onBackButton}>
Back
</ForgeLayout.BackButton>
<ForgeLayout.Breadcrumbs>
<ForgeLayout.Breadcrumb>Breadcrumb One</ForgeLayout.Breadcrumb>
<ForgeLayout.Breadcrumb>Breadcrumb Two</ForgeLayout.Breadcrumb>
<ForgeLayout.Breadcrumb>Breadcrumb Three</ForgeLayout.Breadcrumb>
</ForgeLayout.Breadcrumbs>
</ForgeLayout.BreadcrumbsNavigation>
</ForgeLayout.Controls>
<ForgeLayout.Controls visibleWhenNavStateIs="expanded">
<div>Controls when expanded</div>
<ForgeLayout.ModeSwitcher onModeChange={onModeChange} />
<ForgeLayout.Search />
</ForgeLayout.Controls>
<ForgeLayout.Actions>
<ForgeLayout.MenuAction
Expand Down
90 changes: 46 additions & 44 deletions easy-ui-react/src/ForgeLayout/ForgeLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import {
ForgeLayoutNavSection,
useForgeLayoutNav,
} from "./ForgeLayoutNav";
import { ForgeLayoutHeader } from "./ForgeLayoutHeader";
import {
ForgeLayoutControls,
ForgeLayoutBreadcrumbsNavigation,
ForgeLayoutBackButton,
ForgeLayoutBreadcrumbs,
ForgeLayoutBreadcrumb,
ForgeLayoutSearch,
ForgeLayoutModeSwitcher,
} from "./ForgeLayoutControls";

import styles from "./ForgeLayout.module.scss";

Expand Down Expand Up @@ -46,28 +56,11 @@ export type ForgeLayoutProps = {
backgroundDecoration?: "01";
};

export type ForgeLayoutHeaderProps = {
/** Header children. */
children: ReactNode;
};

export type ForgeLayoutContentProps = {
/** Content children. */
children: ReactNode;
};

export type ForgeLayoutControlsProps = {
/** Controls children. */
children: ReactNode;

/**
* Display state of the nav menu for when these controls show.
*
* @default expanded
*/
visibleWhenNavStateIs?: NavState;
};

export type ForgeLayoutContextType = {
mode?: Mode;
navState?: NavState;
Expand Down Expand Up @@ -114,15 +107,15 @@ export const useForgeLayout = () => {
* </ForgeLayout.Nav>
* <ForgeLayout.Header>
* <ForgeLayout.Controls visibleWhenNavStateIs="collapsed">
* <ForgeLayout.BreadrumbsNavigation>
* <ForgeLayout.BreadcrumbsNavigation>
* <ForgeLayout.BackButton onClick={() => {}}>
* Back
* </ForgeLayout.BackButton>
* <ForgeLayout.Breadrumbs>
* <ForgeLayout.Breadrumb>Breadcrumb</ForgeLayout.Breadrumb>
* <ForgeLayout.Breadrumb>Breadcrumb</ForgeLayout.Breadrumb>
* </ForgeLayout.Breadrumbs>
* </ForgeLayout.BreadrumbsNavigation>
* <ForgeLayout.Breadcrumbs>
* <ForgeLayout.Breadcrumb>Breadcrumb</ForgeLayout.Breadcrumb>
* <ForgeLayout.Breadcrumb>Breadcrumb</ForgeLayout.Breadcrumb>
* </ForgeLayout.Breadcrumbs>
* </ForgeLayout.BreadcrumbsNavigation>
* </ForgeLayout.Controls>
* <ForgeLayout.Controls visibleWhenNavStateIs="expanded">
* <ForgeLayout.ModeSwitcher onModeChange={action("Mode changed!")} />
Expand Down Expand Up @@ -188,27 +181,6 @@ export function ForgeLayout(props: ForgeLayoutProps) {
);
}

function ForgeLayoutHeader(props: ForgeLayoutHeaderProps) {
const { children } = props;
return (
<header className={styles.header}>
<div className={styles.headerBg}></div>
{children}
</header>
);
}

function ForgeLayoutControls(props: ForgeLayoutControlsProps) {
const { navState } = useForgeLayout();
const { children, visibleWhenNavStateIs = "expanded" } = props;

if (navState !== visibleWhenNavStateIs) {
return null;
}

return <div className={styles.controls}>{children}</div>;
}

function ForgeLayoutBody(props: ForgeLayoutContentProps) {
const { children } = props;
return <div className={styles.body}>{children}</div>;
Expand Down Expand Up @@ -249,6 +221,36 @@ ForgeLayout.Header = ForgeLayoutHeader;
*/
ForgeLayout.Controls = ForgeLayoutControls;

/**
* Represents the breadcrumbs and navigation in a `<ForgeLayout />`.
*/
ForgeLayout.BreadcrumbsNavigation = ForgeLayoutBreadcrumbsNavigation;

/**
* Represents a navigation back button in a `<ForgeLayout />`.
*/
ForgeLayout.BackButton = ForgeLayoutBackButton;

/**
* Represents breadcrumbs in a `<ForgeLayout />`.
*/
ForgeLayout.Breadcrumbs = ForgeLayoutBreadcrumbs;

/**
* Represents a breadcrumb in a `<ForgeLayout />`.
*/
ForgeLayout.Breadcrumb = ForgeLayoutBreadcrumb;

/**
* Represents a mode switcher in a `<ForgeLayout />`.
*/
ForgeLayout.ModeSwitcher = ForgeLayoutModeSwitcher;

/**
* Represents a search input in a `<ForgeLayout />`.
*/
ForgeLayout.Search = ForgeLayoutSearch;

/**
* Represents the secondary actions of a `<ForgeLayout />`.
*/
Expand Down
75 changes: 75 additions & 0 deletions easy-ui-react/src/ForgeLayout/ForgeLayoutControls.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@use "../styles/common" as *;
@use "../styles/unstyled";

.controls {
display: flex;
align-items: center;
gap: design-token("space.2");
width: 100%;
}

.breadcrumbNavigationContainer {
border: design-token("shape.border_width.1") solid
theme-token("color.neutral.300");
margin-right: design-token("space.3");
display: inline-flex;
z-index: design-token("z-index.nav");
}

.backButtonContainer {
background: theme-token("color.neutral.050");
border-right: design-token("shape.border_width.1") solid
theme-token("color.neutral.300");
padding: design-token("space.0-5") design-token("space.1.5")
design-token("space.0-5") design-token("space.1");
display: inline-flex;
align-items: center;
}

.backButton {
display: inline-flex;
align-items: center;
cursor: pointer;
color: theme-token("color.primary.600");
}

.breadcrumbsContainer {
padding: design-token("space.0-5") design-token("space.1");
}

.trigger {
@include unstyled.button;
display: flex;
align-items: center;
justify-content: space-between;
gap: design-token("space.1");
border: design-token("shape.border_width.1") solid
theme-token("color.neutral.300");
padding: calc(
#{design-token("space.1")} - #{design-token("shape.border_width.1")}
);
border-radius: design-token("shape.border_radius.md");
cursor: pointer;
width: 100%;
max-width: 133px;
z-index: design-token("z-index.nav");
}

.triggerPopoverOpen {
border-color: theme-token("color.neutral.800");
}

.popover {
border: design-token("shape.border_width.1") solid
theme-token("color.neutral.300");
border-radius: design-token("shape.border_radius.md");
background-color: theme-token("color.neutral.000");
padding: design-token("space.2");
box-shadow: design-token("shadow.overlay");
}

.searchContainer {
width: 100%;
max-width: 715px;
margin-right: design-token("space.3");
}
Loading

0 comments on commit 462b0a4

Please sign in to comment.