Skip to content

Commit

Permalink
feat(BannerNotification)!: introduce 2.0 component (#1904)
Browse files Browse the repository at this point in the history
- add in stories
- add in new component, nested under v2 folder
  • Loading branch information
booc0mtaco authored Mar 25, 2024
1 parent 9dfd62d commit cda9e4b
Show file tree
Hide file tree
Showing 7 changed files with 674 additions and 6 deletions.
87 changes: 87 additions & 0 deletions src/components/BannerNotification/BannerNotification.module.css
Original file line number Diff line number Diff line change
@@ -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);
}

87 changes: 87 additions & 0 deletions src/components/BannerNotification/BannerNotification.stories.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<Button rank="secondary" size="sm" variant="default">
Call to Action
</Button>
),
},
argTypes: {
subTitle: {
control: {
type: 'text',
},
},
callToAction: {
control: {
type: null,
},
},
},
} as Meta<Args>;

type Args = React.ComponentProps<typeof BannerNotification>;

const dismissMethod = () => {
console.log('dismissing~');
};

export const Default: StoryObj<Args> = {};

export const Warning: StoryObj<Args> = {
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> = {
args: {
status: 'critical',
callToAction: (
<Button rank="secondary" size="sm" variant="critical">
Call to Action
</Button>
),
},
};

export const CriticalHorizontal: StoryObj<Args> = {
args: {
status: 'critical',
buttonLayout: 'horizontal',
callToAction: (
<Button rank="secondary" size="sm" variant="critical">
Call to Action
</Button>
),
},
};

export const Favorable: StoryObj<Args> = {
args: {
status: 'favorable',
},
};

export const Dismissable: StoryObj<Args> = {
args: {
onDismiss: dismissMethod,
},
};
6 changes: 6 additions & 0 deletions src/components/BannerNotification/BannerNotification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import * as stories from './BannerNotification.stories';

describe('<BannerNotification />', () => {
generateSnapshots(stories);
});
146 changes: 146 additions & 0 deletions src/components/BannerNotification/BannerNotification.tsx
Original file line number Diff line number Diff line change
@@ -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<Status, IconName> = {
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 (
<aside className={componentClassName}>
<Icon
className={styles['banner-notification__icon']}
name={getIconNameFromStatus(status)}
purpose="decorative"
size="1.5rem"
/>

<div
className={clsx(
styles['banner-notification__body'],
buttonLayout &&
styles[`banner-notification--has-${buttonLayout}-cta`],
)}
>
<div className={styles['banner-notification__text']}>
{title && (
<Heading as="h3" preset="title-md">
{title}
</Heading>
)}
{subTitle && (
<Text
as="p"
className={styles['banner-notification__sub-title']}
preset="body-sm"
>
{subTitle}
</Text>
)}
</div>
{callToAction && (
<div className={styles['banner-notification__call-to-action']}>
{callToAction}
</div>
)}
</div>

{onDismiss && (
<button
className={styles['banner-notification__close-button']}
onClick={onDismiss}
>
<Icon
name="close"
purpose="informative"
size="1.5rem"
title="dismiss notification"
/>
</button>
)}
</aside>
);
};

BannerNotification.displayName = 'BannerNotification';
Loading

0 comments on commit cda9e4b

Please sign in to comment.