Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Toast)!: introduce 2.0 component #1906

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
* 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;
Expand Down
8 changes: 4 additions & 4 deletions src/components/BannerNotification/BannerNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ export type BannerNotificationProps = {
*/
className?: string;
/**
* Callback when banner is dismissed. When passed in, renders banner with a close icon in the top right.
* Callback when notification is dismissed. When passed in, renders banner with a close icon in the top right.
*/
onDismiss?: () => void;
// Design API
/**
*
* Whether the button layout for the call to action is vertical or horizontal.
*/
buttonLayout?: 'vertical' | 'horizontal';
/**
Expand All @@ -42,7 +42,7 @@ export type BannerNotificationProps = {
*/
subTitle?: string;
/**
* The title/heading of the banner
* The title/heading of the notification
*/
title?: string;
};
Expand Down Expand Up @@ -125,7 +125,7 @@ export const BannerNotification = ({
</div>
)}
</div>

{/* TODO-AH: Use `Button` properly */}
{onDismiss && (
<button
className={styles['banner-notification__close-button']}
Expand Down
35 changes: 35 additions & 0 deletions src/components/Toast/Toast-v2.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@import '../../design-tokens/mixins.css';

/*------------------------------------*\
# TOAST
\*------------------------------------*/

.toast {
display: flex;
gap: 1rem;
padding: 1rem;
align-items: center;

border: 0.125rem solid;
border-left: 1rem solid;
border-radius: calc(var(--eds-theme-border-radius-objects-sm) * 1px);


&.toast--status-critical {
color: var(--eds-theme-color-text-utility-critical);
background-color: var(--eds-theme-color-background-utility-critical-low-emphasis);
}

&.toast--status-favorable {
color: var(--eds-theme-color-text-utility-favorable);
background-color: var(--eds-theme-color-background-utility-favorable-low-emphasis);
}
}

.toast__icon {
flex-shrink: 0;
}

.toast__body {
flex-grow: 2;
}
39 changes: 39 additions & 0 deletions src/components/Toast/Toast-v2.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { StoryObj, Meta } from '@storybook/react';
import type { ComponentProps } from 'react';

import { Toast } from './Toast-v2';

export default {
title: 'Components/V2/Toast',
component: Toast,
parameters: {
badges: ['intro-1.0', 'current-2.0'],
},
argTypes: { onDismiss: { action: 'dismissed' } },
args: {
title: "You've got a temporary notification!",
},
} as Meta<Args>;

type Args = ComponentProps<typeof Toast>;

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

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

export const Critical: StoryObj<Args> = {
args: {
status: 'critical',
},
};

export const NotDismissable: StoryObj<Args> = {
args: {
...Default.args,
onDismiss: undefined,
},
};
8 changes: 8 additions & 0 deletions src/components/Toast/Toast-v2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import type { StoryFile } from '@storybook/testing-react';

import * as stories from './Toast.stories';

describe('<Toast /> (v2)', () => {
generateSnapshots(stories as StoryFile);
});
99 changes: 99 additions & 0 deletions src/components/Toast/Toast-v2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import clsx from 'clsx';
import React from 'react';

import type { Status } from '../../util/variant-types';
import { ButtonV2 as Button } from '../Button';
import Icon, { type IconName } from '../Icon';
import Text from '../Text';

import styles from './Toast-v2.module.css';

/**
* TODO-AH:
* - which status is default?
* - default / min / max widths?
*/

export type ToastProps = {
// Component API
/**
* Additional class names that can be appended to the component, passed in for styling.
*/
className?: string;
/**
* Callback when notification is dismissed. When passed in, renders banner with a close icon in the top right.
*/
onDismiss?: () => void;
// Design API
/**
* Keyword to characterize the state of the notification
*/
status?: Extract<Status, 'favorable' | 'critical'>;
/**
* The title/heading of the notification
*/
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 {Toast} from "@chanzuckerberg/eds";`
*
* Toasts display brief, temporary notifications. They're meant to be noticed without disrupting a user's experience or requiring an action to be taken.
*/
export const Toast = ({
className,
onDismiss,
status = 'favorable',
title,
...other
}: ToastProps) => {
const componentClassName = clsx(
styles['toast'],
status && styles[`toast--status-${status}`],
className,
);
return (
<div className={componentClassName} {...other}>
<Icon
className={styles['toast__icon']}
name={getIconNameFromStatus(status)}
purpose="decorative"
size="1.875rem"
/>
<div className={styles['toast__body']}>
<Text as="span" className={styles['toast__text']} preset="title-md">
{title}
</Text>
</div>
{onDismiss && (
<Button
aria-label="close"
context="default"
icon="close"
iconLayout="icon-only"
onClick={onDismiss}
rank="tertiary"
>
Close
</Button>
)}
</div>
);
};
100 changes: 100 additions & 0 deletions src/components/Toast/__snapshots__/Toast-v2.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Toast /> (v2) Error story renders snapshot 1`] = `
<div
class="toast toast--error"
>
<div
class="toast__content"
>
<svg
class="icon"
fill="currentColor"
height="1.5rem"
role="img"
style="--icon-size: 1.5rem;"
viewBox="0 0 24 24"
width="1.5rem"
xmlns="http://www.w3.org/2000/svg"
>
<title>
error
</title>
<path
d="M1 21L12 2L23 21H1ZM11 15H13V10H11V15ZM12 18C12.2833 18 12.521 17.904 12.713 17.712C12.9043 17.5207 13 17.2833 13 17C13 16.7167 12.9043 16.4793 12.713 16.288C12.521 16.096 12.2833 16 12 16C11.7167 16 11.4793 16.096 11.288 16.288C11.096 16.4793 11 16.7167 11 17C11 17.2833 11.096 17.5207 11.288 17.712C11.4793 17.904 11.7167 18 12 18Z"
/>
</svg>
<p
class="toast__text"
>
You've got toast!
</p>
</div>
</div>
`;

exports[`<Toast /> (v2) NotDismissable story renders snapshot 1`] = `
<div
class="toast toast--success"
>
<div
class="toast__content"
>
<svg
class="icon"
fill="currentColor"
height="1.5rem"
role="img"
style="--icon-size: 1.5rem;"
viewBox="0 0 24 24"
width="1.5rem"
xmlns="http://www.w3.org/2000/svg"
>
<title>
success
</title>
<path
d="M12 22C10.6167 22 9.31667 21.7373 8.1 21.212C6.88333 20.6873 5.825 19.975 4.925 19.075C4.025 18.175 3.31267 17.1167 2.788 15.9C2.26267 14.6833 2 13.3833 2 12C2 10.6167 2.26267 9.31667 2.788 8.1C3.31267 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.31233 8.1 2.787C9.31667 2.26233 10.6167 2 12 2C13.3833 2 14.6833 2.26233 15.9 2.787C17.1167 3.31233 18.175 4.025 19.075 4.925C19.975 5.825 20.6873 6.88333 21.212 8.1C21.7373 9.31667 22 10.6167 22 12C22 13.3833 21.7373 14.6833 21.212 15.9C20.6873 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6873 15.9 21.212C14.6833 21.7373 13.3833 22 12 22ZM10.6 16.6L17.65 9.55L16.25 8.15L10.6 13.8L7.75 10.95L6.35 12.35L10.6 16.6Z"
/>
</svg>
<p
class="toast__text"
>
You've got toast!
</p>
</div>
</div>
`;

exports[`<Toast /> (v2) Success story renders snapshot 1`] = `
<div
class="toast toast--success"
>
<div
class="toast__content"
>
<svg
class="icon"
fill="currentColor"
height="1.5rem"
role="img"
style="--icon-size: 1.5rem;"
viewBox="0 0 24 24"
width="1.5rem"
xmlns="http://www.w3.org/2000/svg"
>
<title>
success
</title>
<path
d="M12 22C10.6167 22 9.31667 21.7373 8.1 21.212C6.88333 20.6873 5.825 19.975 4.925 19.075C4.025 18.175 3.31267 17.1167 2.788 15.9C2.26267 14.6833 2 13.3833 2 12C2 10.6167 2.26267 9.31667 2.788 8.1C3.31267 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.31233 8.1 2.787C9.31667 2.26233 10.6167 2 12 2C13.3833 2 14.6833 2.26233 15.9 2.787C17.1167 3.31233 18.175 4.025 19.075 4.925C19.975 5.825 20.6873 6.88333 21.212 8.1C21.7373 9.31667 22 10.6167 22 12C22 13.3833 21.7373 14.6833 21.212 15.9C20.6873 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6873 15.9 21.212C14.6833 21.7373 13.3833 22 12 22ZM10.6 16.6L17.65 9.55L16.25 8.15L10.6 13.8L7.75 10.95L6.35 12.35L10.6 16.6Z"
/>
</svg>
<p
class="toast__text"
>
You've got toast!
</p>
</div>
</div>
`;
Loading