diff --git a/packages/react-components/src/App.tsx b/packages/react-components/src/App.tsx index 107a11fe..bc7b906f 100644 --- a/packages/react-components/src/App.tsx +++ b/packages/react-components/src/App.tsx @@ -7,6 +7,7 @@ import "@bcgov/bc-sans/css/BC_Sans.css"; import { Button, Footer, FooterLinks, Header } from "@/components"; import useWindowDimensions from "@/hooks/useWindowDimensions"; import { + AlertBannerPage, ButtonPage, ButtonGroupPage, CalloutPage, @@ -150,6 +151,7 @@ function App() { + Components diff --git a/packages/react-components/src/components/AlertBanner/AlertBanner.css b/packages/react-components/src/components/AlertBanner/AlertBanner.css new file mode 100644 index 00000000..0f03b532 --- /dev/null +++ b/packages/react-components/src/components/AlertBanner/AlertBanner.css @@ -0,0 +1,88 @@ +.bcds-Alert-Banner { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + width: 100%; + padding: var(--layout-padding-none) var(--layout-padding-medium); +} + +.bcds-Alert-Banner--Container { + flex-grow: 1; + display: flex; + flex-direction: row; + align-items: center; + gap: var(--layout-padding-medium); + max-width: 1100px; + padding: var(--layout-padding-small) var(--layout-padding-none); +} + +.bcds-Alert-Banner--Icon { + align-self: first baseline; + justify-self: flex-start; + padding-top: var(--layout-padding-xsmall); + color: var(--icons-color-primary-invert); +} + +.bcds-Alert-Banner--Content { + display: inline-flex; + gap: var(--layout-margin-medium); + align-items: first baseline; + flex-grow: 1; + flex-wrap: wrap; + font: var(--typography-regular-body); + color: var(--typography-color-primary-invert); +} + +.bcds-Alert-Banner--closeIcon { + align-self: first baseline; +} + +.bcds-Alert-Banner--closeIcon > .bcds-react-aria-Button[data-hovered] svg { + color: var(--icons-color-primary); +} + +.bcds-Alert-Banner--closeIcon svg { + color: var(--icons-color-primary-invert); +} + +/* Variants */ + +/* Info theme */ +.bcds-Alert-Banner.info { + background-color: var(--surface-color-background-dark-blue); +} + +/* Success theme */ +.bcds-Alert-Banner.success { + background-color: var(--support-border-color-success); +} + +/* Warning theme */ +.bcds-Alert-Banner.warning { + background-color: var(--support-border-color-warning); +} + +.bcds-Alert-Banner.warning .bcds-Alert-Banner--Icon { + color: var(--icons-color-primary); +} + +.bcds-Alert-Banner.warning .bcds-Alert-Banner--Content { + color: var(--typography-color-primary); +} + +.bcds-Alert-Banner.warning .bcds-Alert-Banner--closeIcon svg { + color: var(--icons-color-primary); +} + +/* Danger theme */ +.bcds-Alert-Banner.danger { + background-color: var(--support-border-color-danger); +} + +/* Black theme */ + +.bcds-Alert-Banner.black { + background-color: var(--theme-gray-110); +} diff --git a/packages/react-components/src/components/AlertBanner/AlertBanner.tsx b/packages/react-components/src/components/AlertBanner/AlertBanner.tsx new file mode 100644 index 00000000..96122b7e --- /dev/null +++ b/packages/react-components/src/components/AlertBanner/AlertBanner.tsx @@ -0,0 +1,81 @@ +import "./AlertBanner.css"; +import Button from "../Button"; +import SvgCheckCircleIcon from "../Icons/SvgCheckCircleIcon"; +import SvgCloseIcon from "../Icons/SvgCloseIcon"; +import SvgExclamationCircleIcon from "../Icons/SvgExclamationCircleIcon"; +import SvgExclamationIcon from "../Icons/SvgExclamationIcon"; +import SvgInfoIcon from "../Icons/SvgInfoIcon"; + +export interface AlertBannerProps extends React.PropsWithChildren { + /* Sets banner theme */ + variant?: "info" | "success" | "warning" | "danger" | "black"; + /* Hides icon */ + isIconHidden?: boolean; + /* Toggles display of close button */ + isCloseable?: boolean; + /* Sets ARIA role, defaults to 'status' */ + role?: React.AriaRole | undefined; + /* Overrides theme icon */ + customIcon?: React.ReactNode; + /* Controls behaviour of close button */ + onClose?: () => void; +} + +/* Sets correct icon for theme */ +function getIcon(variant: string) { + switch (variant) { + case "info": + return ; + case "black": + return ; + case "success": + return ; + case "warning": + return ; + case "danger": + return ; + default: + return; + } +} + +export default function AlertBanner({ + variant = "info", + isIconHidden = false, + isCloseable = true, + role = "status", + onClose, + children, + customIcon, + ...props +}: AlertBannerProps) { + return ( + + + {" "} + {!isIconHidden && ( + + {customIcon ? customIcon : getIcon(variant)} + + )} + + {children} + + {isCloseable && ( + + + + + + )} + + + ); +} diff --git a/packages/react-components/src/components/AlertBanner/index.ts b/packages/react-components/src/components/AlertBanner/index.ts new file mode 100644 index 00000000..f4ef77ef --- /dev/null +++ b/packages/react-components/src/components/AlertBanner/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AlertBanner"; +export type { AlertBannerProps } from "./AlertBanner"; diff --git a/packages/react-components/src/components/Icons/SvgBcOutlineIcon/SvgBcOutlineIcon.tsx b/packages/react-components/src/components/Icons/SvgBcOutlineIcon/SvgBcOutlineIcon.tsx new file mode 100644 index 00000000..891411f5 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgBcOutlineIcon/SvgBcOutlineIcon.tsx @@ -0,0 +1,23 @@ +export default function SvgBcOutlineIcon({ id = "bc-outline-icon" }) { + return ( + + + + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgBcOutlineIcon/index.ts b/packages/react-components/src/components/Icons/SvgBcOutlineIcon/index.ts new file mode 100644 index 00000000..ec132e11 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgBcOutlineIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgBcOutlineIcon"; diff --git a/packages/react-components/src/components/Icons/SvgBetaIcon/SvgBetaIcon.tsx b/packages/react-components/src/components/Icons/SvgBetaIcon/SvgBetaIcon.tsx new file mode 100644 index 00000000..eabda721 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgBetaIcon/SvgBetaIcon.tsx @@ -0,0 +1,19 @@ +/* The component implements the Flask icon from Font Awesome: https://fontawesome.com/icons/flask */ +export default function SvgBetaIcon({ id = "beta-icon" }) { + return ( + + + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgBetaIcon/index.ts b/packages/react-components/src/components/Icons/SvgBetaIcon/index.ts new file mode 100644 index 00000000..f3ff95e5 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgBetaIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgBetaIcon"; diff --git a/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx b/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx index 122504db..30cf2d41 100644 --- a/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx +++ b/packages/react-components/src/components/Icons/SvgCheckCircleIcon/SvgCheckCircleIcon.tsx @@ -1,3 +1,4 @@ +/* The component implements the Circle Check icon from Font Awesome: https://fontawesome.com/icons/circle-check */ export default function SvgCheckCircleIcon({ id = "check-icon" }) { return ( ({ + banner1: true, + banner2: true, + banner3: true, + banner4: true, + banner5: true, + banner6: true, + }); + + const handleClose = (bannerId: string) => { + setBanners((prev) => ({ + ...prev, + [bannerId]: false, + })); + }; + return ( + <> + {banners.banner1 && ( + handleClose("banner1")}> + Alert banner in its default configuration + + )} + {banners.banner2 && ( + handleClose("banner2")}> + Success theme + + )} + {banners.banner3 && ( + handleClose("banner3")}> + Warning theme with an additional call-to-action button + + Take an action + + + )} + {banners.banner4 && ( + handleClose("banner4")} + > + Danger theme with close button disabled + + )} + {banners.banner5 && ( + } + onClose={() => handleClose("banner5")} + > + Dark theme with a custom icon + + )} + > + ); +} diff --git a/packages/react-components/src/pages/AlertBanner/index.ts b/packages/react-components/src/pages/AlertBanner/index.ts new file mode 100644 index 00000000..c6df1b3b --- /dev/null +++ b/packages/react-components/src/pages/AlertBanner/index.ts @@ -0,0 +1,3 @@ +import AlertBannerPage from "./AlertBanner"; + +export default AlertBannerPage; diff --git a/packages/react-components/src/pages/index.ts b/packages/react-components/src/pages/index.ts index b01e3547..e79ea26b 100644 --- a/packages/react-components/src/pages/index.ts +++ b/packages/react-components/src/pages/index.ts @@ -1,3 +1,4 @@ +import AlertBannerPage from "./AlertBanner"; import ButtonPage from "./Button"; import ButtonGroupPage from "./ButtonGroup"; import CalloutPage from "./Callout"; @@ -13,6 +14,7 @@ import SwitchPage from "./Switch"; import TooltipPage from "./Tooltip"; export { + AlertBannerPage, ButtonPage, ButtonGroupPage, CalloutPage, diff --git a/packages/react-components/src/stories/AlertBanner.mdx b/packages/react-components/src/stories/AlertBanner.mdx new file mode 100644 index 00000000..cea29f44 --- /dev/null +++ b/packages/react-components/src/stories/AlertBanner.mdx @@ -0,0 +1,89 @@ +{/* AlertBanner.mdx */} + +import { + Canvas, + Controls, + Meta, + Primary, + Source, + Story, + Subtitle, +} from "@storybook/blocks"; + +import * as AlertBannerStories from "./AlertBanner.stories"; +import { AlertBanner } from "../components"; + + + +# Alert Banner + + + An alert banner displays an important message about your product or service. + It is intended to be placed at the top of the screen, either above or + immediately below the header or navbar. + + + + +## Usage and resources + +Learn more about working with the alert banner component: + +- [Usage and best practice guidance](https://www2.gov.bc.ca/gov/content?id=8B5DA0E964484966B90D30960AEED39C) +- [View the button component in Figma](https://www2.gov.bc.ca/gov/content?id=8E36BE1D10E04A17B0CD4D913FA7AC43#designers) + +## Controls + + + + +## Configuration + +### Variants + +The alert banner ships with a set of themes designed for different use-cases, configured via the `variant` prop. Each theme applies a preset colourway and icon. If no `variant` prop is passed, the banner defaults to `info`: + + + + + + + +**Note**: if you need to modify the styling of an alert banner's content, you can write additional rules for the `.bcds-Alert-Banner--Container` CSS class. + +### Alert content + +Pass content to the `children` slot to populate an alert banner. + +You can also pass in additional components if needed. For example, you can add a call to action button: + + + +### Closing an alert banner + +By default, an alert banner displays a 'close' button. 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. + +You can disable the close button by setting the `isCloseable` prop to `false`: + + + +### Icons + +Pass a component to the `customIcon` slot to override the predefined icon: + + + +Use the `isIconHidden` prop to hide the theme icon entirely: + + + +### ARIA roles + +By default, an alert banner renders with the [ARIA "status" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/status_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 we recommend using either "status" or "[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/AlertBanner.stories.tsx b/packages/react-components/src/stories/AlertBanner.stories.tsx new file mode 100644 index 00000000..31216a12 --- /dev/null +++ b/packages/react-components/src/stories/AlertBanner.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { AlertBanner } from "../components"; +import { AlertBannerProps } from "@/components/AlertBanner"; +import { Button } from "@/components"; +import { SvgBcOutlineIcon } from "@/components"; + +const meta = { + title: "Components/AlertBanner/AlertBanner", + component: AlertBanner, + argTypes: { + variant: { + options: ["info", "success", "warning", "danger", "black"], + control: { type: "radio" }, + description: "Sets the theme and icon for the alert", + }, + children: { + control: { type: "object" }, + description: "Populates the content of the alert", + }, + customIcon: { + control: { type: "object" }, + description: "Overrides default icon", + }, + isIconHidden: { + control: { type: "boolean" }, + description: "Toggles display of alert icon", + }, + isCloseable: { + control: { type: "boolean" }, + description: "Toggles display of close button", + }, + role: { + control: { type: "text" }, + description: "Sets ARIA role for the alert", + }, + onClose: { + control: { type: "object" }, + description: "Function for the close button", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const AlertBannerTemplate: Story = { + args: { + variant: "info", + isIconHidden: false, + isCloseable: true, + role: "status", + children: ["This is an alert banner in its default configuration"], + onClose: () => alert("onClose()"), + }, + render: ({ ...args }: AlertBannerProps) => , +}; + +export const SuccessBanner: Story = { + ...AlertBannerTemplate, + args: { + variant: "success", + children: ["This banner uses the 'success' theme"], + }, +}; + +export const WarningBanner: Story = { + ...AlertBannerTemplate, + args: { + variant: "warning", + children: ["This banner uses the 'warning' theme"], + }, +}; + +export const DangerBanner: Story = { + ...AlertBannerTemplate, + args: { + variant: "danger", + children: ["This banner uses the 'danger' theme"], + }, +}; + +export const DarkBanner: Story = { + ...AlertBannerTemplate, + args: { + variant: "black", + children: ["This banner uses the 'black' theme"], + }, +}; + +export const BannerWithCustomIcon: Story = { + ...AlertBannerTemplate, + args: { + customIcon: [], + children: ["This alert banner has a custom icon"], + }, +}; + +export const BannerWithoutIcon: Story = { + ...AlertBannerTemplate, + args: { + isIconHidden: true, + children: ["This banner has its theme icon disabled"], + }, +}; + +export const BannerWithButton: Story = { + ...AlertBannerTemplate, + args: { + children: [ + "This alert banner also renders a call to action using a Button component", + + Take an action + , + ], + }, +}; + +export const UncloseableBanner: Story = { + ...AlertBannerTemplate, + args: { + isCloseable: false, + children: ["The close button is disabled on this alert banner"], + }, +};