Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ForgeLayout): add modeswitcher, breadcrumb navigation, and … #1550

Merged
merged 10 commits into from
Dec 23, 2024
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
78 changes: 40 additions & 38 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 @@ -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 *;
OskiTheCoder marked this conversation as resolved.
Show resolved Hide resolved
@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: 1;
}

.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: 1;
}

.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
Loading