Skip to content

Commit

Permalink
feat: add application layout components (#1090)
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Jul 2, 2024
1 parent d0401d4 commit 6ca33ff
Show file tree
Hide file tree
Showing 30 changed files with 1,442 additions and 1 deletion.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"version": "0.47.3",
"main": "dist/index.js",
"module": "dist/index.js",
"author": "Huw Wilkins <huw.wilkins@canonical.com>",
"author": {
"email": "webteam@canonical.com",
"name": "Canonical Webteam"
},
"license": "LGPL-3.0",
"files": [
"dist/**/*.js",
Expand Down
139 changes: 139 additions & 0 deletions src/components/ApplicationLayout/AppAside/AppAside.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* eslint-disable react-hooks/rules-of-hooks */
import React, { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react";

import Application from "components/ApplicationLayout/Application";
import AppMain from "components/ApplicationLayout/AppMain";
import Button from "components/Button";
import Col from "components/Col";
import Form from "components/Form";
import Icon from "components/Icon";
import Input from "components/Input";
import Panel from "components/Panel";
import Row from "components/Row";

import AppAside from "./AppAside";

const meta: Meta<typeof AppAside> = {
component: AppAside,
tags: ["autodocs"],
argTypes: {
children: {
control: false,
},
},
};

export default meta;

type Story = StoryObj<typeof AppAside>;

/**
* In most common cases an `AppAside` should contain a `<Panel>` to display the
* content as intended in the application layout.
*
* `AppAside` should be a direct child of an `<Application>` or passed to the
* application layout `<ApplicationLayout aside={<AppAside .../>}>`.
*/
export const Default: Story = {
render: (args) => {
const [pinned, setPinned] = useState(false);
const [width, setWidth] = useState(null);
const [collapsed, setCollapsed] = useState(false);
return (
<Application>
<AppMain>
<p>Scroll to the right to see the panel.</p>
<Button onClick={() => setCollapsed(false)}>Open</Button>
<Button onClick={() => setWidth("narrow")}>Narrow</Button>
<Button onClick={() => setWidth(null)}>Default</Button>
<Button onClick={() => setWidth("wide")}>Wide</Button>
</AppMain>
<AppAside
{...args}
pinned={pinned}
wide={width === "wide"}
narrow={width === "narrow"}
collapsed={collapsed}
>
<Panel
controls={
<>
<Button
onClick={() => setPinned(!pinned)}
dense
className="u-no-margin--bottom"
>
Pin
</Button>
<Button
appearance="base"
className="u-no-margin--bottom"
hasIcon
onClick={() => setCollapsed(!collapsed)}
>
<Icon name="close">Close</Icon>
</Button>
</>
}
title="App aside"
>
<Form stacked>
<Input
label="Full name"
type="text"
name="fullName"
autoComplete="name"
stacked
/>
<Input
label="Username"
type="text"
name="username-stacked"
autoComplete="username"
aria-describedby="exampleHelpTextMessage"
stacked
help="30 characters or fewer."
/>
<Input
type="text"
label="Email address"
aria-invalid="true"
name="username-stackederror"
autoComplete="email"
required
error="This field is required."
stacked
/>
<Input
label="Address line 1"
type="text"
name="address-optional-stacked"
autoComplete="address-line1"
stacked
/>
<Input
label="Address line 2"
type="text"
name="address-optional-stacked"
autoComplete="address-line3"
stacked
/>
<Row>
<Col size={12}>
<Button
appearance="positive"
className="u-float-right"
name="add-details"
>
Add details
</Button>
</Col>
</Row>
</Form>
</Panel>
</AppAside>
</Application>
);
},
};
24 changes: 24 additions & 0 deletions src/components/ApplicationLayout/AppAside/AppAside.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import { render, screen } from "@testing-library/react";

import AppAside from "./AppAside";

it("displays collapsed", async () => {
render(<AppAside collapsed>Content</AppAside>);
expect(screen.queryByRole("complementary")).toHaveClass("is-collapsed");
});

it("displays as narrow", async () => {
render(<AppAside narrow>Content</AppAside>);
expect(screen.queryByRole("complementary")).toHaveClass("is-narrow");
});

it("displays pinned", async () => {
render(<AppAside pinned>Content</AppAside>);
expect(screen.queryByRole("complementary")).toHaveClass("is-pinned");
});

it("displays as wide", async () => {
render(<AppAside wide>Content</AppAside>);
expect(screen.queryByRole("complementary")).toHaveClass("is-wide");
});
63 changes: 63 additions & 0 deletions src/components/ApplicationLayout/AppAside/AppAside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import type { PropsWithSpread } from "types";
import classNames from "classnames";
import { type HTMLProps, type PropsWithChildren } from "react";

export type Props = PropsWithSpread<
{
/**
* Whether the aside panel should be collapsed. Toggling this state will animate
* the panel open or closed.
*/
collapsed?: boolean;
/**
* The panel content.
*/
children?: PropsWithChildren["children"];
/**
* A ref that will be passed to the wrapping `<aside>` element.
*/
forwardRef?: React.Ref<HTMLElement> | null;
/**
* Whether the aside panel should be narrow.
*/
narrow?: boolean;
/**
* Whether the aside panel should be pinned. When pinned the panel will appear
* beside the main content, instead of above it.
*/
pinned?: boolean;
/**
* Whether the aside panel should be wide.
*/
wide?: boolean;
},
HTMLProps<HTMLElement>
>;

const AppAside = ({
children,
className,
collapsed,
narrow,
forwardRef,
pinned,
wide,
...props
}: Props) => {
return (
<aside
className={classNames("l-aside", className, {
"is-collapsed": collapsed,
"is-narrow": narrow,
"is-pinned": pinned,
"is-wide": wide,
})}
{...props}
ref={forwardRef}
>
{children}
</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";
65 changes: 65 additions & 0 deletions src/components/ApplicationLayout/AppMain/AppMain.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { Meta, StoryObj } from "@storybook/react";

import Application from "components/ApplicationLayout/Application";
import Button from "components/Button";
import React from "react";
import Panel from "components/Panel";

import AppMain from "./AppMain";

const meta: Meta<typeof AppMain> = {
component: AppMain,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof AppMain>;

/**
* In most common cases an `AppMain` should contain a `<Panel>` to display the
* content as intended in the application layout.
*
* `AppMain` should be a direct child of an `<Application>` or when using `ApplicationLayout`
* it will automatically wrap the component's children.
*/
export const Default: Story = {
args: {
children: "AppMain",
},
};

export const Content: Story = {
render: (args) => {
return (
<Application>
<AppMain {...args}>
<Panel
controls={
<>
<Button
appearance="positive"
onClick={() => {}}
className="u-no-margin--bottom"
>
Add
</Button>
<Button
appearance="negative"
onClick={() => {}}
className="u-no-margin--bottom"
>
Delete
</Button>
</>
}
title="App main"
>
<p>App main content.</p>
<p>Scroll to the right to see the controls.</p>
</Panel>
</AppMain>
</Application>
);
},
};
10 changes: 10 additions & 0 deletions src/components/ApplicationLayout/AppMain/AppMain.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from "react";
import { render, screen } from "@testing-library/react";

import AppMain from "./AppMain";

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

export type Props = {
/**
* The main content.
*/
children?: PropsWithChildren["children"];
} & HTMLProps<HTMLDivElement>;

/**
* This is a [React](https://reactjs.org/) component for main content area in the Vanilla
* [Application Layout](https://vanillaframework.io/docs/layouts/application).
*/
const AppMain = ({ children, className, ...props }: Props) => {
return (
<main className={classNames("l-main", className)} {...props}>
{children}
</main>
);
};

export default AppMain;
2 changes: 2 additions & 0 deletions src/components/ApplicationLayout/AppMain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./AppMain";
export type { Props as AppMainProps } from "./AppMain";
Loading

0 comments on commit 6ca33ff

Please sign in to comment.