Skip to content

Commit

Permalink
Add application layout components.
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Jan 24, 2024
1 parent 91e1814 commit c7a3f25
Show file tree
Hide file tree
Showing 39 changed files with 1,247 additions and 0 deletions.
21 changes: 21 additions & 0 deletions src/components/ApplicationLayout/AppAside/AppAside.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { renderComponent } from "testing/utils";

import AppAside from "./AppAside";

it("displays without a close", async () => {
renderComponent(<AppAside>Content</AppAside>);
expect(
screen.queryByRole("button", { name: "Close" }),

Check failure on line 11 in src/components/ApplicationLayout/AppAside/AppAside.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
).not.toBeInTheDocument();
});

it("displays a close button", async () => {
const onClose = jest.fn();
renderComponent(<AppAside onClose={onClose} />);
expect(screen.getByText("Close")).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Close" }));
expect(onClose).toHaveBeenCalled();
});
59 changes: 59 additions & 0 deletions src/components/ApplicationLayout/AppAside/AppAside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Button from "@canonical/react-components/dist/components/Button";
import Icon from "@canonical/react-components/dist/components/Icon";
import type { PropsWithSpread } from "@canonical/react-components/dist/types";
import classNames from "classnames";
import { type HTMLProps, type PropsWithChildren } from "react";

import Panel, { type PanelProps } from "components/upstream/Panel";

export type Props = PropsWithSpread<
{
forwardRef?: React.Ref<HTMLElement> | null;
onClose?: () => void;
panelProps?: PanelProps;
pinned?: boolean;
} & PropsWithChildren,
HTMLProps<HTMLElement>
>;

const AppAside = ({
children,
className,
forwardRef,
onClose,
panelProps,
pinned,
...props
}: Props) => {
return (
<aside
className={classNames("l-aside", className, {
"is-pinned": pinned,
})}
{...props}
ref={forwardRef}
>
<Panel
{...(panelProps ?? {})}
controls={
<>
{panelProps?.controls}
{onClose ? (
<Button
appearance="base"
className="u-no-margin--bottom"
hasIcon
onClick={() => onClose()}
>
<Icon name="close">Close</Icon>
</Button>
) : null}
</>
}
>
{children}
</Panel>
</aside>
);
};
export default AppAside;
2 changes: 2 additions & 0 deletions src/components/ApplicationLayout/AppAside/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./AppAside";
export type { Props as AppAsideProps } from "./AppAside";
11 changes: 11 additions & 0 deletions src/components/ApplicationLayout/AppMain/AppMain.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { screen } from "@testing-library/react";

import { renderComponent } from "testing/utils";

import AppMain from "./AppMain";

it("displays children", () => {
const children = "Test content";
renderComponent(<AppMain>{children}</AppMain>);
expect(screen.getByText(children)).toBeInTheDocument();
});
14 changes: 14 additions & 0 deletions src/components/ApplicationLayout/AppMain/AppMain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import classNames from "classnames";
import type { HTMLProps, PropsWithChildren } from "react";

type Props = PropsWithChildren & HTMLProps<HTMLDivElement>;

const AppMain = ({ children, className, ...props }: Props) => {
return (
<main className={classNames("l-main", className)} {...props}>
{children}
</main>
);
};

export default AppMain;
1 change: 1 addition & 0 deletions src/components/ApplicationLayout/AppMain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./AppMain";
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { screen } from "@testing-library/react";

import { renderComponent } from "testing/utils";

import AppNavigation from "./AppNavigation";

it("displays children", () => {
const children = "Test content";
renderComponent(<AppNavigation>{children}</AppNavigation>);
expect(screen.getByText(children)).toBeInTheDocument();
});

it("displays as collapsed", () => {
const { result } = renderComponent(<AppNavigation collapsed />);
expect(result.container.firstChild).toHaveClass("is-collapsed");

Check warning on line 15 in src/components/ApplicationLayout/AppNavigation/AppNavigation.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});

it("displays as pinned", () => {
const { result } = renderComponent(<AppNavigation pinned />);
expect(result.container.firstChild).toHaveClass("is-pinned");

Check warning on line 20 in src/components/ApplicationLayout/AppNavigation/AppNavigation.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});
33 changes: 33 additions & 0 deletions src/components/ApplicationLayout/AppNavigation/AppNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { PropsWithSpread } from "@canonical/react-components/dist/types";
import classNames from "classnames";
import type { HTMLProps, PropsWithChildren } from "react";

type Props = PropsWithSpread<
{
collapsed?: boolean;
pinned?: boolean;
} & PropsWithChildren,
HTMLProps<HTMLDivElement>
>;

const AppNavigation = ({
children,
className,
collapsed,
pinned,
...props
}: Props) => {
return (
<header
className={classNames("l-navigation", className, {
"is-collapsed": collapsed,
"is-pinned": pinned,
})}
{...props}
>
<div className="l-navigation__drawer">{children}</div>
</header>
);
};

export default AppNavigation;
1 change: 1 addition & 0 deletions src/components/ApplicationLayout/AppNavigation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./AppNavigation";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { screen } from "@testing-library/react";

import { renderComponent } from "testing/utils";

import AppNavigationBar from "./AppNavigationBar";

it("displays children", () => {
const children = "Test content";
renderComponent(<AppNavigationBar>{children}</AppNavigationBar>);
expect(screen.getByText(children)).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { PropsWithSpread } from "@canonical/react-components/dist/types";
import classNames from "classnames";
import type { HTMLProps, PropsWithChildren } from "react";

type Props = PropsWithSpread<PropsWithChildren, HTMLProps<HTMLDivElement>>;

const AppNavigationBar = ({ children, className, ...props }: Props) => {
return (
<header className={classNames("l-navigation-bar", className)} {...props}>
{children}
</header>
);
};

export default AppNavigationBar;
1 change: 1 addition & 0 deletions src/components/ApplicationLayout/AppNavigationBar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./AppNavigationBar";
11 changes: 11 additions & 0 deletions src/components/ApplicationLayout/AppStatus/AppStatus.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { screen } from "@testing-library/react";

import { renderComponent } from "testing/utils";

import AppStatus from "./AppStatus";

it("displays children", () => {
const children = "Test content";
renderComponent(<AppStatus>{children}</AppStatus>);
expect(screen.getByText(children)).toBeInTheDocument();
});
16 changes: 16 additions & 0 deletions src/components/ApplicationLayout/AppStatus/AppStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import classNames from "classnames";
import type { HTMLProps, PropsWithChildren } from "react";

import Panel from "components/upstream/Panel";

type Props = PropsWithChildren & HTMLProps<HTMLDivElement>;

const AppStatus = ({ children, className, ...props }: Props) => {
return (
<aside className={classNames("l-status", className)} {...props}>
<Panel wrapContent={false}>{children}</Panel>
</aside>
);
};

export default AppStatus;
1 change: 1 addition & 0 deletions src/components/ApplicationLayout/AppStatus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./AppStatus";
11 changes: 11 additions & 0 deletions src/components/ApplicationLayout/Application/Application.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { screen } from "@testing-library/react";

import { renderComponent } from "testing/utils";

import Application from "./Application";

it("displays children", () => {
const children = "Test content";
renderComponent(<Application>{children}</Application>);
expect(screen.getByText(children)).toBeInTheDocument();
});
19 changes: 19 additions & 0 deletions src/components/ApplicationLayout/Application/Application.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PropsWithSpread } from "@canonical/react-components/dist/types";
import classNames from "classnames";
import type { HTMLProps, PropsWithChildren } from "react";

type Props = PropsWithSpread<PropsWithChildren, HTMLProps<HTMLDivElement>>;

const Application = ({ children, className, ...props }: Props) => {
return (
<div
className={classNames("l-application", className)}
role="presentation"
{...props}
>
{children}
</div>
);
};

export default Application;
1 change: 1 addition & 0 deletions src/components/ApplicationLayout/Application/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./Application";
116 changes: 116 additions & 0 deletions src/components/ApplicationLayout/ApplicationLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { renderComponent } from "testing/utils";

import AppAside from "./AppAside";
import ApplicationLayout from "./ApplicationLayout";

const logo = {
icon: "icon.svg",
href: "http://example.com",
name: "name.svg",
nameAlt: "Juju",
};

it("displays a logo", () => {
renderComponent(<ApplicationLayout logo={logo} navItems={[]} />);
const link = screen.getAllByRole("link", { name: "Juju" })[0];
expect(within(link).getByRole("img", { name: "Juju" })).toHaveAttribute(
"src",
"name.svg",

Check failure on line 21 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
});

it("displays as light", () => {
renderComponent(<ApplicationLayout dark={false} logo={logo} navItems={[]} />);
expect(document.querySelectorAll(".is-dark")).toHaveLength(0);

Check warning on line 27 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
expect(document.querySelectorAll(".is-light")).toHaveLength(0);

Check warning on line 28 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});

it("displays as dark", () => {
renderComponent(<ApplicationLayout dark logo={logo} navItems={[]} />);
expect(document.querySelectorAll(".is-dark")).toHaveLength(5);

Check warning on line 33 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
// Two icons are light so that they appear over the dark background.
expect(document.querySelectorAll(".is-light")).toHaveLength(2);

Check warning on line 35 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});

it("displays main content", () => {
const content = "Main content";
renderComponent(
<ApplicationLayout logo={logo} navItems={[]}>
{content}
</ApplicationLayout>,

Check failure on line 43 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
expect(screen.getByText(content)).toBeInTheDocument();
});

it("displays a status bar", () => {
const content = "Main content";
renderComponent(
<ApplicationLayout logo={logo} navItems={[]} status={content} />,

Check failure on line 51 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
expect(screen.getByText(content)).toBeInTheDocument();
expect(screen.getByText(content).parentNode).toHaveClass("l-status");

Check warning on line 54 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});

it("displays an aside", () => {
const content = "Aside content";
renderComponent(
<ApplicationLayout
logo={logo}
navItems={[]}
aside={<AppAside>{content}</AppAside>}
/>,

Check failure on line 64 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
expect(screen.getByText(content)).toBeInTheDocument();
expect(document.querySelector(".l-aside")).toBeInTheDocument();

Check warning on line 67 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});

it("pins the menu", async () => {
renderComponent(<ApplicationLayout logo={logo} navItems={[]} />);
expect(document.querySelector(".l-navigation")).not.toHaveClass("is-pinned");

Check warning on line 72 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
await userEvent.click(screen.getByRole("button", { name: "Pin menu" }));
expect(document.querySelector(".l-navigation")).toHaveClass("is-pinned");

Check warning on line 74 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Avoid direct Node access. Prefer using the methods from Testing Library
});

it("pins the menu using external state", async () => {
const onPinMenu = jest.fn();
renderComponent(
<ApplicationLayout
logo={logo}
navItems={[]}
menuPinned={true}
onPinMenu={onPinMenu}
/>,

Check failure on line 85 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
expect(document.querySelector(".l-navigation")).toHaveClass("is-pinned");
await userEvent.click(screen.getByRole("button", { name: "Unpin menu" }));
expect(onPinMenu).toHaveBeenCalledWith(false);
});

it("opens and collapses the menu", async () => {
renderComponent(<ApplicationLayout logo={logo} navItems={[]} />);
expect(document.querySelector(".l-navigation")).toHaveClass("is-collapsed");
await userEvent.click(screen.getByRole("button", { name: "Menu" }));
expect(document.querySelector(".l-navigation")).not.toHaveClass(
"is-collapsed",

Check failure on line 97 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
await userEvent.click(screen.getByRole("button", { name: "Close menu" }));
expect(document.querySelector(".l-navigation")).toHaveClass("is-collapsed");
});

it("collapses the menu using external state", async () => {
const onCollapseMenu = jest.fn();
renderComponent(
<ApplicationLayout
logo={logo}
navItems={[]}
menuCollapsed={true}
onCollapseMenu={onCollapseMenu}
/>,

Check failure on line 111 in src/components/ApplicationLayout/ApplicationLayout.test.tsx

View workflow job for this annotation

GitHub Actions / Lint, build and test

Delete `,`
);
expect(document.querySelector(".l-navigation")).toHaveClass("is-collapsed");
await userEvent.click(screen.getByRole("button", { name: "Menu" }));
expect(onCollapseMenu).toHaveBeenCalledWith(false);
});
Loading

0 comments on commit c7a3f25

Please sign in to comment.