From aed0f0941d9d09f8a7b96f2219f6801eefa4ee50 Mon Sep 17 00:00:00 2001 From: Andrew Holloway Date: Thu, 22 Aug 2024 13:25:17 -0500 Subject: [PATCH] feat(config): add transition durations for tailwind (#2036) - add in durations via (transitionDuration) - add in new toast notification implementation example - use new medium durations in example - demo autodismiss behavior with stack of notifications - use state to track notifications --- .../ToastNotification.stories.ts | 63 -------- .../ToastNotification.stories.tsx | 149 ++++++++++++++++++ tailwind.config.ts | 16 ++ 3 files changed, 165 insertions(+), 63 deletions(-) delete mode 100644 src/components/ToastNotification/ToastNotification.stories.ts create mode 100644 src/components/ToastNotification/ToastNotification.stories.tsx diff --git a/src/components/ToastNotification/ToastNotification.stories.ts b/src/components/ToastNotification/ToastNotification.stories.ts deleted file mode 100644 index ebda7629c..000000000 --- a/src/components/ToastNotification/ToastNotification.stories.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { StoryObj, Meta } from '@storybook/react'; -import type { ComponentProps } from 'react'; - -import { ToastNotification } from './ToastNotification'; - -export default { - title: 'Components/ToastNotification', - component: ToastNotification, - parameters: { - layout: 'centered', - badges: ['intro-1.0', 'current-2.0'], - }, - argTypes: { - onDismiss: { action: 'trigger dismiss' }, - timeout: { table: { disable: true } }, - }, - args: { - title: "You've got a temporary notification!", - className: 'w-96', - }, -} as Meta; - -type Args = ComponentProps; - -export const Default: StoryObj = {}; - -export const Favorable: StoryObj = { - args: { - status: 'favorable', - }, -}; - -/** - * Notifications can have different status, to indicate errors or destructive actions have completed. - */ -export const Critical: StoryObj = { - args: { - status: 'critical', - }, -}; - -/** - * We can restrict the ability to dismiss the notification by not specifying the `onDismiss` method. - */ -export const NotDismissable: StoryObj = { - args: { - ...Default.args, - onDismiss: undefined, - }, -}; - -/** - * Tooltips can be instructed to auto-close after a certain period. After the timeout, the component will call the defined - * `onDismiss` method. The behavior of the dissmisal is left up to the user, which allows for complete control. - */ -export const AutoDismiss: StoryObj = { - args: { - ...Default.args, - dissmissType: 'auto', - timeout: 500, - onDismiss: () => console.log('trigger onDismiss'), - }, -}; diff --git a/src/components/ToastNotification/ToastNotification.stories.tsx b/src/components/ToastNotification/ToastNotification.stories.tsx new file mode 100644 index 000000000..129fff522 --- /dev/null +++ b/src/components/ToastNotification/ToastNotification.stories.tsx @@ -0,0 +1,149 @@ +import { Transition } from '@headlessui/react'; +import type { StoryObj, Meta } from '@storybook/react'; +import React from 'react'; + +import type { ComponentProps } from 'react'; + +import { ToastNotification } from './ToastNotification'; + +import Button from '../Button'; + +export default { + title: 'Components/ToastNotification', + component: ToastNotification, + parameters: { + layout: 'centered', + badges: ['intro-1.0', 'current-2.0'], + }, + argTypes: { + onDismiss: { action: 'trigger dismiss' }, + timeout: { table: { disable: true } }, + }, + args: { + title: "You've got a temporary notification!", + className: 'w-96', + }, +} as Meta; + +type Args = ComponentProps; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Favorable: Story = { + args: { + status: 'favorable', + }, +}; + +/** + * Notifications can have different status, to indicate errors or destructive actions have completed. + */ +export const Critical: Story = { + args: { + status: 'critical', + }, +}; + +/** + * We can restrict the ability to dismiss the notification by not specifying the `onDismiss` method. + */ +export const NotDismissable: Story = { + args: { + ...Default.args, + onDismiss: undefined, + }, +}; + +/** + * Tooltips can be instructed to auto-close after a certain period. After the timeout, the component will call the defined + * `onDismiss` method. The behavior of the dissmisal is left up to the user, which allows for complete control. + */ +export const AutoDismiss: Story = { + args: { + ...Default.args, + dissmissType: 'auto', + timeout: 500, + onDismiss: () => console.log('trigger onDismiss'), + }, +}; + +let toastId = 0; +const ToastNotificationManager = (args: Args) => { + const [toasts, setToasts] = React.useState< + { id: number | string; text: string; show?: boolean }[] + >([]); + + // TODO: clean up `toasts` after .show is set to false (using useEffect? and .debounce) + // - In a production implementation, you can filter out any toasts where show=false + + return ( +
+ +
+ {toasts.map((toast) => ( + + { + setToasts( + toasts.map((thisToast) => { + return thisToast.id === toast.id + ? { ...thisToast, show: false } + : thisToast; + }), + ); + }} + title={'You got a new toast: ' + toast.text + toast.id} + /> + + ))} +
+
+ ); +}; + +/** + * This implementation example shows how you can use toasts with state to handle multiple, stacking notifications. + * + * For a full, production-ready implementation, clean up any toasts with show=false after the animation has completed. + * - Consider using lodash.debounce to time the re-render, and useEffect that watches the list of toasts + * - Any debouncing should map to whatever duration is used in `Transition` + * + * Here, we use `` provided by [HeadlessUI](https://github.com/chanzuckerberg/edu-design-system/blob/main/package.json#L91-L93). + */ +export const ExampleDismissingToasts: Story = { + render: (args) => , + parameters: { + // For interactive use, low value in snap testing again since already covered in other stories. + chromatic: { disableSnapshot: true }, + snapshot: { skip: true }, + }, +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 90f225cc6..48f9567b2 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -11,6 +11,19 @@ const { // Add a type to the token sizes to avoid literals for keys const sizes: { [x: string]: string } = edsTokens.size; +// add a type to the token sizes for movement durations +const movement: { [x: string]: string } = { + ...Object.keys(edsTokens.anim.move) + .map((movement) => { + return { [movement]: `${edsTokens.anim.move[movement]}s` }; + }) + .reduce((accumulate, current) => { + const entry = Object.entries(current)[0]; + accumulate[entry[0]] = entry[1]; + return accumulate; + }, {}), +}; + const sizeTokens = { // We pull the spacing tokens and format them such that names are like 'size-${name} = ${value}px' ...Object.keys(sizes) @@ -52,6 +65,9 @@ export default { spacing: { ...sizeTokens, }, + transitionDuration: { + ...movement, + }, }, fontWeight: { normal: edsTokens['font-weight'].light,