diff --git a/src/components/BannerNotification/BannerNotification.module.css b/src/components/BannerNotification/BannerNotification.module.css new file mode 100644 index 000000000..566143f22 --- /dev/null +++ b/src/components/BannerNotification/BannerNotification.module.css @@ -0,0 +1,87 @@ +@import '../../design-tokens/mixins.css'; + +/*------------------------------------*\ + # BANNER NOTIFICATION +\*------------------------------------*/ + +/** + * Message of information, success, caution, or warning to the user. + */ +.banner { + /* Position is relative to allow for absolute-positioned close button. */ + position: relative; + /* Grid is used to separate the icon from the text with correct spacing. */ + display: flex; + gap: 1rem; + padding: 1rem; + + border: 0.125rem solid; + border-left: 1rem solid; + border-radius: calc(var(--eds-theme-border-radius-objects-sm) * 1px); + + &.banner-notification--status-informational { + color: var(--eds-theme-color-text-utility-informational); + background-color: var(--eds-theme-color-background-utility-information-low-emphasis); + } + + &.banner-notification--status-critical { + color: var(--eds-theme-color-text-utility-critical); + background-color: var(--eds-theme-color-background-utility-critical-low-emphasis); + } + + &.banner-notification--status-favorable { + color: var(--eds-theme-color-text-utility-favorable); + background-color: var(--eds-theme-color-background-utility-favorable-low-emphasis); + } + + &.banner-notification--status-warning { + color: var(--eds-theme-color-text-utility-warning); + background-color: var(--eds-theme-color-background-utility-warning-low-emphasis); + } +} + +.banner-notification__icon { + flex-shrink: 0; +} + +.banner-notification__body { + flex-grow: 2; + display: flex; + + &.banner-notification--has-vertical-cta { + flex-direction: column; + } + + &.banner-notification--has-horizontal-cta { + flex-direction: row; + } +} + +.banner-notification__call-to-action { + .banner-notification--has-vertical-cta & { + margin-top: 1rem; + } + + .banner-notificatino-has-horizontal-cta & { + margin-left: 1rem; + } +} + +.banner-notification__text { + flex-grow: 2; +} + +/** + * Close button + * + * Button used to dismiss a banner. + */ +.banner-notification__close-button { + align-self: flex-start; + color: var(--eds-theme-color-text-utility-default-secondary); +} + +.banner-notification__sub-title { + color: var(--eds-theme-color-text-utility-default-secondary); +} + diff --git a/src/components/BannerNotification/BannerNotification.stories.tsx b/src/components/BannerNotification/BannerNotification.stories.tsx new file mode 100644 index 000000000..2b8be10f1 --- /dev/null +++ b/src/components/BannerNotification/BannerNotification.stories.tsx @@ -0,0 +1,87 @@ +import type { StoryObj, Meta } from '@storybook/react'; +import React from 'react'; + +import { BannerNotification } from './BannerNotification'; +import { ButtonV2 as Button } from '../Button'; + +export default { + title: 'Components/V2/BannerNotification', + component: BannerNotification, + parameters: { + badges: ['intro-1.0', 'current-2.0'], + }, + args: { + title: 'Alert title which communicates info to the user', + subTitle: ' Subtitle which provides additional detail', + callToAction: ( + + ), + }, + argTypes: { + subTitle: { + control: { + type: 'text', + }, + }, + callToAction: { + control: { + type: null, + }, + }, + }, +} as Meta; + +type Args = React.ComponentProps; + +const dismissMethod = () => { + console.log('dismissing~'); +}; + +export const Default: StoryObj = {}; + +export const Warning: StoryObj = { + args: { + status: 'warning', + }, +}; + +/** + * When using critical, make sure `Button` has a matching variant specified. + * TODO-AH: design of the secondary critical button has a customization to the background that needs verification. + */ +export const Critical: StoryObj = { + args: { + status: 'critical', + callToAction: ( + + ), + }, +}; + +export const CriticalHorizontal: StoryObj = { + args: { + status: 'critical', + buttonLayout: 'horizontal', + callToAction: ( + + ), + }, +}; + +export const Favorable: StoryObj = { + args: { + status: 'favorable', + }, +}; + +export const Dismissable: StoryObj = { + args: { + onDismiss: dismissMethod, + }, +}; diff --git a/src/components/BannerNotification/BannerNotification.test.ts b/src/components/BannerNotification/BannerNotification.test.ts new file mode 100644 index 000000000..d3dccd066 --- /dev/null +++ b/src/components/BannerNotification/BannerNotification.test.ts @@ -0,0 +1,6 @@ +import { generateSnapshots } from '@chanzuckerberg/story-utils'; +import * as stories from './BannerNotification.stories'; + +describe('', () => { + generateSnapshots(stories); +}); diff --git a/src/components/BannerNotification/BannerNotification.tsx b/src/components/BannerNotification/BannerNotification.tsx new file mode 100644 index 000000000..bdb28bcd9 --- /dev/null +++ b/src/components/BannerNotification/BannerNotification.tsx @@ -0,0 +1,146 @@ +import clsx from 'clsx'; +import React, { type ReactNode } from 'react'; + +import type { Status } from '../../util/variant-types'; +import Heading from '../Heading'; +import Icon, { type IconName } from '../Icon'; +import Text from '../Text'; + +import styles from './BannerNotification.module.css'; + +/** + * TODO-AH: + * - feedback on api naming in figma + * - handling of aria-live for a11y + */ + +export type BannerNotificationProps = { + // Component API + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Callback when banner is dismissed. When passed in, renders banner with a close icon in the top right. + */ + onDismiss?: () => void; + // Design API + /** + * + */ + buttonLayout?: 'vertical' | 'horizontal'; + /** + * TODO-AH: ensure this is a button + */ + callToAction?: ReactNode; + /** + * Keyword to characterize the state of the notification + */ + status?: Status; + /** + * Secondary text used to describe the notification in more detail + */ + subTitle?: string; + /** + * The title/heading of the banner + */ + title?: string; +}; + +/** + * Map statuses to existing icon names + * TODO-AH: de-dupe this with the function in InlineNotification + * + * @param status component status + * @returns the matching icon name + */ +function getIconNameFromStatus(status: Status): IconName { + const map: Record = { + informational: 'info', + critical: 'dangerous', + warning: 'warning', + favorable: 'check-circle', + }; + return map[status]; +} + +/** + * `import {BannerNotification} from "@chanzuckerberg/eds";` + * + * An alert placed at the top of a page which impacts the entire experience on a screen. + */ +export const BannerNotification = ({ + buttonLayout = 'vertical', + callToAction, + className, + subTitle, + onDismiss, + status = 'informational', + title, +}: BannerNotificationProps) => { + const componentClassName = clsx( + // Base styles + styles['banner'], + status && styles[`banner-notification--status-${status}`], + // Other options + onDismiss && styles['banner--dismissable'], + className, + ); + + return ( + + ); +}; + +BannerNotification.displayName = 'BannerNotification'; diff --git a/src/components/BannerNotification/__snapshots__/BannerNotification.test.ts.snap b/src/components/BannerNotification/__snapshots__/BannerNotification.test.ts.snap new file mode 100644 index 000000000..e3c8761a9 --- /dev/null +++ b/src/components/BannerNotification/__snapshots__/BannerNotification.test.ts.snap @@ -0,0 +1,340 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Critical story renders snapshot 1`] = ` + +`; + +exports[` CriticalHorizontal story renders snapshot 1`] = ` + +`; + +exports[` Default story renders snapshot 1`] = ` + +`; + +exports[` Dismissable story renders snapshot 1`] = ` + +`; + +exports[` Favorable story renders snapshot 1`] = ` + +`; + +exports[` Warning story renders snapshot 1`] = ` + +`; diff --git a/src/components/BannerNotification/index.ts b/src/components/BannerNotification/index.ts new file mode 100644 index 000000000..434f43b80 --- /dev/null +++ b/src/components/BannerNotification/index.ts @@ -0,0 +1,2 @@ +export { BannerNotification as default } from './BannerNotification'; +export type { BannerNotificationProps } from './BannerNotification'; diff --git a/src/components/InlineNotification/InlineNotification-v2.tsx b/src/components/InlineNotification/InlineNotification-v2.tsx index a307a9d3a..9c37caa78 100644 --- a/src/components/InlineNotification/InlineNotification-v2.tsx +++ b/src/components/InlineNotification/InlineNotification-v2.tsx @@ -22,17 +22,17 @@ type InlineNotificationProps = { className?: string; // Design API /** - * The text contents of the tag, nested inside the component, in addition to the icon. - */ - title: string; - /** - * Text used for the main description of the notification + * Keyword to characterize the state of the notification */ status?: Status; /** * Secondary text used to describe the notification in more detail */ subTitle?: string; + /** + * The text contents of the tag, nested inside the component, in addition to the icon. + */ + title: string; }; /** @@ -53,7 +53,7 @@ function getIconNameFromStatus(status: Status): IconName { /** * `import {InlineNotification} from "@chanzuckerberg/eds";` * - * This component provides an inline banner accompanied with an icon for messaging users. + * An alert placed within a section of a page to provide a contextual notification. For example, an error which applies to multiple fields within a form. */ export const InlineNotification = ({ className,