Skip to content

Commit

Permalink
Add ButtonGroup component and refactor Inline Alert (#446)
Browse files Browse the repository at this point in the history
* component scaffold

* fleshing out component

* add alignment prop and styles

* add built-in buttonGroup slot to inline alert

* clean up vite and storybook examples

* add default layout behaviour

* storybook docs and examples

* storybook docs and examples

* typo

* aria-label support

* docs clarification

* fix aria-labelledby declaration

* storybook docs cleanup

* use small buttons in alert example
  • Loading branch information
mkernohanbc authored Aug 13, 2024
1 parent 144c083 commit 040e2b7
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 16 deletions.
2 changes: 2 additions & 0 deletions packages/react-components/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Button, Footer, FooterLinks, Header } from "@/components";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import {
ButtonPage,
ButtonGroupPage,
InlineAlertPage,
SelectPage,
TagGroupPage,
Expand Down Expand Up @@ -148,6 +149,7 @@ function App() {
<main>
<h1>Components</h1>
<ButtonPage />
<ButtonGroupPage />
<SwitchPage />
<InlineAlertPage />
<SelectPage />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.bcds-ButtonGroup {
display: flex;
gap: var(--layout-margin-small);
flex-grow: 1;
}

.bcds-ButtonGroup.horizontal {
flex-direction: row;
}

.bcds-ButtonGroup.vertical {
flex-direction: column;
}

.bcds-ButtonGroup.start {
justify-content: flex-start;
}

.bcds-ButtonGroup.center {
justify-content: center;
}

.bcds-ButtonGroup.end {
justify-content: flex-end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import "./ButtonGroup.css";

export interface ButtonGroupProps extends React.PropsWithChildren {
/* Sets layout of button group */
orientation?: "horizontal" | "vertical";
/* Sets alignment of button group */
alignment?: "start" | "center" | "end";
/* Semantic label for button group */
ariaLabel?: string | undefined;
}

export default function ButtonGroup({
orientation = "horizontal",
alignment = "start",
ariaLabel,
children,
...props
}: ButtonGroupProps) {
return (
<div
className={`bcds-ButtonGroup ${orientation} ${alignment}`}
role="group"
aria-label={ariaLabel}
{...props}
>
{children}
</div>
);
}
2 changes: 2 additions & 0 deletions packages/react-components/src/components/ButtonGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./ButtonGroup";
export type { ButtonGroupProps } from "./ButtonGroup";
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,8 @@
.bcds-Inline-Alert.danger > .bcds-Inline-Alert--icon {
color: var(--icons-color-danger);
}

/* Button group */
.bcds-Inline-Alert--container > .bcds-ButtonGroup {
padding-top: var(--layout-padding-small);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import "./InlineAlert.css";
import {
Button,
ButtonGroup,
SvgInfoIcon,
SvgCheckCircleIcon,
SvgExclamationIcon,
Expand All @@ -16,6 +17,8 @@ export interface InlineAlertProps extends React.PropsWithChildren {
title?: string;
/* Alert description */
description?: string;
/* Button group */
buttons?: React.ReactNode;
/* Alert closeable state */
isCloseable?: boolean;
/* Show or hide left icon */
Expand Down Expand Up @@ -48,6 +51,7 @@ export default function InlineAlert({
isIconHidden = false,
isCloseable = false,
role = "note",
buttons,
children,
onClose,
...props
Expand All @@ -60,7 +64,7 @@ export default function InlineAlert({
<div
className="bcds-Inline-Alert--container"
role={role}
aria-labelledby="alert-title"
aria-labelledby={"alert-title"}
>
{children ? (
children
Expand All @@ -72,6 +76,11 @@ export default function InlineAlert({
</span>
)}
{description && <span className="description">{description}</span>}
{buttons && (
<ButtonGroup alignment="end" orientation="horizontal">
{buttons}
</ButtonGroup>
)}
</>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "@bcgov/design-tokens/css/variables.css";

export { default as InlineAlert } from "./InlineAlert";
export { default as Button } from "./Button";
export { default as ButtonGroup } from "./ButtonGroup";
export { default as Header } from "./Header";
export { default as Footer, FooterLinks } from "./Footer";
export { default as Form } from "./Form";
Expand Down
15 changes: 15 additions & 0 deletions packages/react-components/src/pages/ButtonGroup/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Button, ButtonGroup } from "@/components";

export default function ButtonGroupPage() {
return (
<>
<h2>ButtonGroup</h2>
<ButtonGroup>
<Button variant="primary">Button 1</Button>
<Button variant="secondary">Button 2</Button>
<Button variant="tertiary">Button 3</Button>
<Button variant="link">Button 4</Button>
</ButtonGroup>
</>
);
}
3 changes: 3 additions & 0 deletions packages/react-components/src/pages/ButtonGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ButtonGroupPage from "./ButtonGroup";

export default ButtonGroupPage;
20 changes: 9 additions & 11 deletions packages/react-components/src/pages/InlineAlert/InlineAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,14 @@ export default function InlineAlertPage() {
<InlineAlert
variant="info"
title="This is an alert that also has additional actions"
description="It uses button components in the children slot to display additional important actions that the user can take."
children={[
<>
<Button variant="secondary" size="small">
This is a secondary button
</Button>
<Button variant="primary" size="small">
This is a primary button
</Button>
</>,
description="It uses button components to display additional important actions that the user can take."
buttons={[
<Button variant="secondary" size="small">
This is a secondary button
</Button>,
<Button variant="primary" size="small">
This is a primary button
</Button>,
]}
/>
<InlineAlert
Expand All @@ -53,7 +51,7 @@ export default function InlineAlertPage() {
/>
<InlineAlert
variant="info"
hideIcon={true}
isIconHidden={true}
title="This alert has no icon"
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ButtonPage from "./Button";
import ButtonGroupPage from "./ButtonGroup";
import InlineAlertPage from "./InlineAlert";
import SelectPage from "./Select";
import TagGroupPage from "./TagGroup";
Expand All @@ -9,6 +10,7 @@ import TooltipPage from "./Tooltip";

export {
ButtonPage,
ButtonGroupPage,
InlineAlertPage,
SelectPage,
TagGroupPage,
Expand Down
62 changes: 62 additions & 0 deletions packages/react-components/src/stories/ButtonGroup.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{/* ButtonGroup.mdx */}

import {
Canvas,
Controls,
Meta,
Primary,
Source,
Story,
Subtitle,
} from "@storybook/blocks";

import * as ButtonGroupStories from "./ButtonGroup.stories";
import * as InlineAlertStories from "./InlineAlert.stories";

<Meta of={ButtonGroupStories} />

# ButtonGroup

<Subtitle>Layout component for buttons</Subtitle>

<Source
code={`import { ButtonGroup } from "@bcgov/design-system-react-components";`}
language="typescript"
/>

## Usage and resources

ButtonGroup is a utility component intended for use when multiple buttons need to be rendered alongside one another. It contains basic styling to ensure proper spacing between the buttons.

ButtonGroup renders a `<div>` with the [ARIA "group" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/group_role) and a user-defined `aria-label`. It expects an array of [buttons](/docs/components-button-button--docs), passed via the `children` prop.

### Example

ButtonGroup is used to populate buttons within the [Inline Alert component](/docs/components-inline-alert-inline-alert--docs):

<Canvas of={InlineAlertStories.AlertWithButtons} />

## Controls

<Canvas of={ButtonGroupStories.ButtonGroupTemplate} />
<Controls of={ButtonGroupStories.ButtonGroupTemplate} />

## Layout

By default, ButtonGroup renders buttons in a horizontal row, starting from the left edge.

### Orientation

Use the `orientation` prop to toggle the orientation of the button group between `horizontal` and `vertical` layouts:

<Canvas of={ButtonGroupStories.VerticalButtonGroup} />

### Alignment

Use the `alignment` prop to set the alignment of the buttons within the group:

- `start` (left or top, depending on orientation)
- `center`
- `end` (right or bottom, depending on orientation)

<Canvas of={ButtonGroupStories.CenteredButtonGroup} />
70 changes: 70 additions & 0 deletions packages/react-components/src/stories/ButtonGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from "@storybook/react";

import { Button, ButtonGroup } from "../components";
import { ButtonGroupProps } from "@/components/ButtonGroup";

const meta = {
title: "Components/Button/ButtonGroup",
component: ButtonGroup,
parameters: {
layout: "centered",
},
argTypes: {
orientation: {
options: ["horizontal", "vertical"],
control: { type: "radio" },
description: "Layout of button group as a whole",
},
alignment: {
options: ["start", "center", "end"],
control: { type: "radio" },
description: "Alignment of buttons within group",
},
ariaLabel: {
control: { type: "text" },
description: "Semantic label for the button group",
},
},
} satisfies Meta<typeof ButtonGroup>;

export default meta;
type Story = StoryObj<typeof meta>;

export const ButtonGroupTemplate: Story = {
args: {
alignment: "start",
orientation: "horizontal",
ariaLabel: "A group of buttons",
children: [
<Button variant="primary">Button 1</Button>,
<Button variant="secondary">Button 2</Button>,
<Button variant="secondary">Button 3</Button>,
],
},
render: ({ ...args }: ButtonGroupProps) => <ButtonGroup {...args} />,
};

export const VerticalButtonGroup: Story = {
...ButtonGroupTemplate,
args: {
orientation: "vertical",
children: [
<Button variant="primary">Button 1</Button>,
<Button variant="secondary">Button 2</Button>,
<Button variant="secondary">Button 3</Button>,
],
},
};

export const CenteredButtonGroup: Story = {
...ButtonGroupTemplate,
args: {
orientation: "horizontal",
alignment: "center",
children: [
<Button variant="primary">Button 1</Button>,
<Button variant="secondary">Button 2</Button>,
<Button variant="secondary">Button 3</Button>,
],
},
};
10 changes: 8 additions & 2 deletions packages/react-components/src/stories/InlineAlert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,21 @@ If no `variant` prop is passed, the alert defaults to the `info` theme.

### Alert content

Use the `title` and optional `description` props to populate an alert's content:
Use the `title` and optional `description` props to populate an alert's content. `title` provides an accessible name for the alert, using [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby).

<Canvas of={InlineAlertStories.InlineAlertTemplate} />

Use the `buttons` prop to pass an array of button components, which are rendered inside a [ButtonGroup](/docs/components-button-button-group--docs):

<Canvas of={InlineAlertStories.AlertWithButtons} />

Pass `isIconHidden` to hide the theme icon on the left:

<Canvas of={InlineAlertStories.InlineAlertWithoutIcon} />

Alternatively, you can use pass components and content into the `children` slot, replacing the default `title` and `description` pattern with your own composition.
#### Overriding alert layout

To override the standard `title`, `description` and `buttons` composition, you can pass your own components and content via the `children` slot.

### Closeable alerts

Expand Down
Loading

0 comments on commit 040e2b7

Please sign in to comment.