diff --git a/packages/react-components/src/App.tsx b/packages/react-components/src/App.tsx index bf864f74..c52b11ac 100644 --- a/packages/react-components/src/App.tsx +++ b/packages/react-components/src/App.tsx @@ -8,6 +8,7 @@ import { Button, Footer, FooterLinks, Header } from "@/components"; import useWindowDimensions from "@/hooks/useWindowDimensions"; import { ButtonPage, + InlineAlertPage, SelectPage, TagGroupPage, TooltipPage, @@ -146,6 +147,7 @@ function App() {

Components

+ diff --git a/packages/react-components/src/components/Footer/Footer.tsx b/packages/react-components/src/components/Footer/Footer.tsx index 1a457c71..8e938368 100644 --- a/packages/react-components/src/components/Footer/Footer.tsx +++ b/packages/react-components/src/components/Footer/Footer.tsx @@ -1,6 +1,6 @@ import React from "react"; -import SvgBcLogo from "../SvgBcLogo"; +import SvgBcLogo from "../Icons/SvgBcLogo"; import "./Footer.css"; diff --git a/packages/react-components/src/components/Header/Header.tsx b/packages/react-components/src/components/Header/Header.tsx index 00c9e813..f5bafbeb 100644 --- a/packages/react-components/src/components/Header/Header.tsx +++ b/packages/react-components/src/components/Header/Header.tsx @@ -1,6 +1,6 @@ import React from "react"; -import SvgBcLogo from "../SvgBcLogo"; +import SvgBcLogo from "../Icons/SvgBcLogo"; import "./Header.css"; diff --git a/packages/react-components/src/components/SvgBcLogo/SvgBcLogo.tsx b/packages/react-components/src/components/Icons/SvgBcLogo/SvgBcLogo.tsx similarity index 100% rename from packages/react-components/src/components/SvgBcLogo/SvgBcLogo.tsx rename to packages/react-components/src/components/Icons/SvgBcLogo/SvgBcLogo.tsx diff --git a/packages/react-components/src/components/SvgBcLogo/index.ts b/packages/react-components/src/components/Icons/SvgBcLogo/index.ts similarity index 100% rename from packages/react-components/src/components/SvgBcLogo/index.ts rename to packages/react-components/src/components/Icons/SvgBcLogo/index.ts diff --git a/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx b/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx new file mode 100644 index 00000000..122504db --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx @@ -0,0 +1,18 @@ +export default function SvgCheckCircleIcon({ id = "check-icon" }) { + return ( + + + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgCheckCircleIcon/index.ts b/packages/react-components/src/components/Icons/SvgCheckCircleIcon/index.ts new file mode 100644 index 00000000..28e7e6d2 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgCheckCircleIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgCheckCircleIcon"; diff --git a/packages/react-components/src/components/Icons/SvgCloseIcon/SvgCloseIcon.tsx b/packages/react-components/src/components/Icons/SvgCloseIcon/SvgCloseIcon.tsx new file mode 100644 index 00000000..cb4e9833 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgCloseIcon/SvgCloseIcon.tsx @@ -0,0 +1,18 @@ +export default function SvgCloseIcon({ id = "close-icon" }) { + return ( + + + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgCloseIcon/index.ts b/packages/react-components/src/components/Icons/SvgCloseIcon/index.ts new file mode 100644 index 00000000..526fa3d6 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgCloseIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgCloseIcon"; diff --git a/packages/react-components/src/components/Icons/SvgExclamationCircleIcon/SvgExclamationCircleIcon.tsx b/packages/react-components/src/components/Icons/SvgExclamationCircleIcon/SvgExclamationCircleIcon.tsx new file mode 100644 index 00000000..72ec7104 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgExclamationCircleIcon/SvgExclamationCircleIcon.tsx @@ -0,0 +1,20 @@ +export default function SvgExclamationCircleIcon({ + id = "exclamation-icon-circle", +}) { + return ( + + + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgExclamationCircleIcon/index.ts b/packages/react-components/src/components/Icons/SvgExclamationCircleIcon/index.ts new file mode 100644 index 00000000..b13240de --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgExclamationCircleIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgExclamationCircleIcon"; diff --git a/packages/react-components/src/components/SvgExclamationIcon/SvgExclamationIcon.tsx b/packages/react-components/src/components/Icons/SvgExclamationIcon/SvgExclamationIcon.tsx similarity index 100% rename from packages/react-components/src/components/SvgExclamationIcon/SvgExclamationIcon.tsx rename to packages/react-components/src/components/Icons/SvgExclamationIcon/SvgExclamationIcon.tsx diff --git a/packages/react-components/src/components/SvgExclamationIcon/index.ts b/packages/react-components/src/components/Icons/SvgExclamationIcon/index.ts similarity index 100% rename from packages/react-components/src/components/SvgExclamationIcon/index.ts rename to packages/react-components/src/components/Icons/SvgExclamationIcon/index.ts diff --git a/packages/react-components/src/components/Icons/SvgInfoIcon/SvgInfoIcon.tsx b/packages/react-components/src/components/Icons/SvgInfoIcon/SvgInfoIcon.tsx new file mode 100644 index 00000000..38ad12a0 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgInfoIcon/SvgInfoIcon.tsx @@ -0,0 +1,18 @@ +export default function SvgInfoIcon({ id = "information-icon" }) { + return ( + + + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgInfoIcon/index.ts b/packages/react-components/src/components/Icons/SvgInfoIcon/index.ts new file mode 100644 index 00000000..8b1582ea --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgInfoIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgInfoIcon"; diff --git a/packages/react-components/src/components/InlineAlert/InlineAlert.css b/packages/react-components/src/components/InlineAlert/InlineAlert.css new file mode 100644 index 00000000..998c2858 --- /dev/null +++ b/packages/react-components/src/components/InlineAlert/InlineAlert.css @@ -0,0 +1,96 @@ +.bcds-Inline-Alert { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--layout-margin-small); + padding: var(--layout-padding-medium) var(--layout-padding-large); +} + +/* Alert content */ +.bcds-Inline-Alert--container { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.bcds-Inline-Alert--container > .title { + font: var(--typography-bold-body); + color: var(--typography-color-primary); +} + +.bcds-Inline-Alert--container > .description { + font: var(--typography-regular-body); + color: var(--typography-color-primary); +} + +/* Left icon */ +.bcds-Inline-Alert--icon { + display: inline-flex; + align-self: flex-start; + padding-top: var(--layout-padding-xsmall); +} + +.bcds-Inline-Alert--icon > svg { + min-width: var(--icons-size-medium); + height: var(--icons-size-medium); +} + +/* Close icon button */ +.bcds-Inline-Alert--closeIcon { + display: inline-flex; + align-self: first baseline; + color: var(--icons-color-primary); +} + +.bcds-Inline-Alert--closeIcon > svg { + min-width: var(--icons-size-medium); + height: var(--icons-size-medium); +} + +/* Info variant */ +.bcds-Inline-Alert.info { + background-color: var(--support-surface-color-info); + border: var(--layout-border-width-small) solid + var(--support-border-color-info); + border-radius: var(--layout-border-radius-medium); +} + +.bcds-Inline-Alert.info > .bcds-Inline-Alert--icon { + color: var(--icons-color-info); +} + +/* Success variant */ +.bcds-Inline-Alert.success { + background-color: var(--suport-surface-color-success); + border: var(--layout-border-width-small) solid + var(--support-border-color-success); + border-radius: var(--layout-border-radius-medium); +} + +.bcds-Inline-Alert.success > .bcds-Inline-Alert--icon { + color: var(--icons-color-success); +} + +/* Warning variant */ +.bcds-Inline-Alert.warning { + background-color: var(--support-surface-color-warning); + border: var(--layout-border-width-small) solid + var(--support-border-color-warning); + border-radius: var(--layout-border-radius-medium); +} + +.bcds-Inline-Alert.warning > .bcds-Inline-Alert--icon { + color: var(--icons-color-warning); +} + +/* Danger variant */ +.bcds-Inline-Alert.danger { + background-color: var(--support-surface-color-danger); + border: var(--layout-border-width-small) solid + var(--support-border-color-danger); + border-radius: var(--layout-border-radius-medium); +} + +.bcds-Inline-Alert.danger > .bcds-Inline-Alert--icon { + color: var(--icons-color-danger); +} diff --git a/packages/react-components/src/components/InlineAlert/InlineAlert.tsx b/packages/react-components/src/components/InlineAlert/InlineAlert.tsx new file mode 100644 index 00000000..9ab5c4d5 --- /dev/null +++ b/packages/react-components/src/components/InlineAlert/InlineAlert.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import "./InlineAlert.css"; +import { + Button, + SvgInfoIcon, + SvgCheckCircleIcon, + SvgExclamationIcon, + SvgExclamationCircleIcon, + SvgCloseIcon, +} from "@/components"; + +export interface InlineAlertProps extends React.PropsWithChildren { + /* Alert theme */ + variant?: "info" | "success" | "warning" | "danger"; + /* Alert title */ + title?: string; + /* Alert description */ + description?: string; + /* Alert closeable state */ + isCloseable?: boolean; + /* Show or hide left icon */ + isIconHidden?: boolean; + /* ARIA role */ + role?: React.AriaRole | undefined; + /* Close button handling */ + onClose?: () => void; +} + +function getIcon(variant: string) { + switch (variant) { + case "info": + return ; + case "success": + return ; + case "warning": + return ; + case "danger": + return ; + default: + return; + } +} + +export default function InlineAlert({ + variant = "info", + title, + description, + isIconHidden = false, + isCloseable = false, + role = "note", + children, + onClose, + ...props +}: InlineAlertProps) { + return ( +
+ {!isIconHidden && ( + {getIcon(variant)} + )} +
+ {children ? ( + children + ) : ( + <> + {title && ( + + {title} + + )} + {description && {description}} + + )} +
+ {isCloseable && ( + + + + )} +
+ ); +} diff --git a/packages/react-components/src/components/InlineAlert/index.ts b/packages/react-components/src/components/InlineAlert/index.ts new file mode 100644 index 00000000..444fc79e --- /dev/null +++ b/packages/react-components/src/components/InlineAlert/index.ts @@ -0,0 +1,2 @@ +export { default } from "./InlineAlert"; +export type { InlineAlertProps } from "./InlineAlert"; diff --git a/packages/react-components/src/components/Select/Select.css b/packages/react-components/src/components/Select/Select.css index 157d83b7..b8b4f5ed 100644 --- a/packages/react-components/src/components/Select/Select.css +++ b/packages/react-components/src/components/Select/Select.css @@ -17,7 +17,7 @@ color: var(--typography-color-disabled); } -/* Rext description below select input */ +/* Text description below select input */ .bcds-react-aria-Select--Description { font: var(--typography-regular-small-body); color: var(--typography-color-secondary); diff --git a/packages/react-components/src/components/TagGroup/TagGroup.tsx b/packages/react-components/src/components/TagGroup/TagGroup.tsx index 728d6637..e84e1438 100644 --- a/packages/react-components/src/components/TagGroup/TagGroup.tsx +++ b/packages/react-components/src/components/TagGroup/TagGroup.tsx @@ -6,7 +6,8 @@ import { } from "react-aria-components"; import "./TagGroup.css"; -import SvgExclamationIcon from "../SvgExclamationIcon"; + +import SvgExclamationIcon from "../Icons/SvgExclamationIcon"; export interface TagGroupProps extends ReactAriaTagGroupProps { /** diff --git a/packages/react-components/src/components/TextArea/TextArea.tsx b/packages/react-components/src/components/TextArea/TextArea.tsx index c2e69199..6f334a6b 100644 --- a/packages/react-components/src/components/TextArea/TextArea.tsx +++ b/packages/react-components/src/components/TextArea/TextArea.tsx @@ -10,7 +10,7 @@ import { } from "react-aria-components"; import "./TextArea.css"; -import SvgExclamationIcon from "../SvgExclamationIcon"; +import SvgExclamationIcon from "../Icons/SvgExclamationIcon"; export interface TextAreaProps extends ReactAriaTextFieldProps { /* Sets text label above text input field */ diff --git a/packages/react-components/src/components/index.ts b/packages/react-components/src/components/index.ts index 6421894b..a2714636 100644 --- a/packages/react-components/src/components/index.ts +++ b/packages/react-components/src/components/index.ts @@ -1,11 +1,17 @@ import "@bcgov/design-tokens/css/variables.css"; +export { default as InlineAlert } from "./InlineAlert"; export { default as Button } from "./Button"; export { default as Header } from "./Header"; export { default as Footer, FooterLinks } from "./Footer"; export { default as Form } from "./Form"; export { default as Select } from "./Select"; -export { default as SvgBcLogo } from "./SvgBcLogo"; +export { default as SvgBcLogo } from "./Icons/SvgBcLogo"; +export { default as SvgCheckCircleIcon } from "./Icons/SvgCheckCircleIcon"; +export { default as SvgExclamationIcon } from "./Icons/SvgExclamationIcon"; +export { default as SvgExclamationCircleIcon } from "./Icons/SvgExclamationCircleIcon"; +export { default as SvgCloseIcon } from "./Icons/SvgCloseIcon"; +export { default as SvgInfoIcon } from "./Icons/SvgInfoIcon"; export { default as Tag } from "./Tag"; export { default as TagGroup } from "./TagGroup"; export { default as TagList } from "./TagList"; diff --git a/packages/react-components/src/pages/InlineAlert/InlineAlert.tsx b/packages/react-components/src/pages/InlineAlert/InlineAlert.tsx new file mode 100644 index 00000000..02f57bb1 --- /dev/null +++ b/packages/react-components/src/pages/InlineAlert/InlineAlert.tsx @@ -0,0 +1,62 @@ +import { Button, InlineAlert } from "@/components"; + +export default function InlineAlertPage() { + return ( + <> +

Inline Alert

+
+ + + + + + + + , + ]} + /> + + +
+ + ); +} diff --git a/packages/react-components/src/pages/InlineAlert/index.ts b/packages/react-components/src/pages/InlineAlert/index.ts new file mode 100644 index 00000000..13380fc9 --- /dev/null +++ b/packages/react-components/src/pages/InlineAlert/index.ts @@ -0,0 +1,3 @@ +import InlineAlertPage from "./InlineAlert"; + +export default InlineAlertPage; diff --git a/packages/react-components/src/pages/index.ts b/packages/react-components/src/pages/index.ts index 3f1dac80..7f18be03 100644 --- a/packages/react-components/src/pages/index.ts +++ b/packages/react-components/src/pages/index.ts @@ -1,8 +1,17 @@ import ButtonPage from "./Button"; +import InlineAlertPage from "./InlineAlert"; import SelectPage from "./Select"; import TagGroupPage from "./TagGroup"; import TextAreaPage from "./TextArea"; import TextFieldPage from "./TextField"; import TooltipPage from "./Tooltip"; -export { ButtonPage, SelectPage, TagGroupPage, TextAreaPage, TextFieldPage, TooltipPage }; +export { + ButtonPage, + InlineAlertPage, + SelectPage, + TagGroupPage, + TextAreaPage, + TextFieldPage, + TooltipPage, +}; diff --git a/packages/react-components/src/stories/InlineAlert.mdx b/packages/react-components/src/stories/InlineAlert.mdx new file mode 100644 index 00000000..d461d174 --- /dev/null +++ b/packages/react-components/src/stories/InlineAlert.mdx @@ -0,0 +1,99 @@ +{/* Inline Alert.mdx */} + +import { + Canvas, + Controls, + Meta, + Primary, + Source, + Story, + Subtitle, +} from "@storybook/blocks"; + +import * as InlineAlertStories from "./InlineAlert.stories"; + + + +# Inline Alert + +An inline alert displays an important message to the user. + + + +## Usage and resources + +Learn more about working with the inline alert component: + +- [Usage and best practice guidance]() +- [View the select component in Figma](https://www2.gov.bc.ca/gov/content?id=8E36BE1D10E04A17B0CD4D913FA7AC43#designers) + +## Controls + + + + +## Configuration + +### Variants + +The inline alert ships with 4 themes for different use cases. Each theme has a different colour scheme and icon. + +The alert theme is set by the `variant` prop: + +- `info` +- `success` +- `warning` +- `danger` + +If no `variant` prop is passed, the alert defaults to the `info` theme. + +#### Info + + + +#### Success + + + +#### Warning + + + +#### Danger + + + +### Alert content + +Use the `title` and optional `description` props to populate an alert's content: + + + +Pass `isIconHidden` to hide the theme icon on the left: + + + +Alternatively, you can use pass components and content into the `children` slot, replacing the default `title` and `description` pattern with your own composition. + +### Closeable alerts + +Pass `isCloseable` to display a close button on the right-hand side of the alert: + + + +Use the `onClose` prop to pass a callback function that is called when the close button is pressed. Use `onClose` to configure the close/hide behaviour with your app's state management library. + +### ARIA roles + +By default, an inline alert renders with the [ARIA "note" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/note_role). + +Use the `role` prop to set a different role. You can pass [any valid ARIA role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles), but these are the recommended options: + +- [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) +- [status](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_role) +- [alert](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role) + +**Note**: the "alert" role should be used with caution, and only when the alert requires the user's immediate attention. diff --git a/packages/react-components/src/stories/InlineAlert.stories.tsx b/packages/react-components/src/stories/InlineAlert.stories.tsx new file mode 100644 index 00000000..93cc2355 --- /dev/null +++ b/packages/react-components/src/stories/InlineAlert.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { InlineAlert } from "../components"; +import { InlineAlertProps } from "@/components/InlineAlert"; + +const meta = { + title: "Components/Inline Alert/Inline Alert", + component: InlineAlert, + parameters: { + layout: "centered", + }, + argTypes: { + variant: { + options: ["info", "success", "warning", "danger"], + control: { type: "radio" }, + description: "Sets the theme of the alert", + }, + title: { + control: { type: "text" }, + description: "Sets the alert title", + }, + description: { + control: { type: "text" }, + description: "Sets the alert text", + }, + isIconHidden: { + control: { type: "boolean" }, + description: "Show or hide the left icon", + }, + isCloseable: { + control: { type: "boolean" }, + description: "Whether an alert can be closed", + }, + role: { + control: { type: "text" }, + description: "Sets ARIA role for the alert", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const InlineAlertTemplate: Story = { + args: { + variant: "info", + title: "This is an alert title", + description: + "The alert description is used to provide additional explanatory or helper text.", + }, + render: ({ ...args }: InlineAlertProps) => , +}; + +export const InfoAlert: Story = { + args: { + variant: "info", + title: "This is an alert with the 'info' theme", + description: + "This is the default alert theme. Use it to provide a generic informational alert.", + }, +}; + +export const SuccessAlert: Story = { + args: { + variant: "success", + title: "This is an alert with the 'success' theme", + description: + "Use this alert theme to indicate a positive or success state.", + }, +}; + +export const WarningAlert: Story = { + args: { + variant: "warning", + title: "This is an alert with the 'warning' theme", + description: + "Use this alert theme to indicate a non-urgent problem or notice.", + }, +}; + +export const DangerAlert: Story = { + args: { + variant: "danger", + title: "This is an alert with the 'danger' theme", + description: + "Use this alert theme to communicate an urgent warning or error to the user", + }, +}; + +export const InlineAlertWithoutIcon: Story = { + args: { + title: "This alert has no visible icon", + isIconHidden: true, + }, +}; + +export const CloseableAlert: Story = { + args: { + title: "This alert is closeable", + isCloseable: true, + }, +};