diff --git a/src/components/Accordion/Accordion.stories.tsx b/src/components/Accordion/Accordion.stories.tsx index 9f169a742..59f28d5c2 100644 --- a/src/components/Accordion/Accordion.stories.tsx +++ b/src/components/Accordion/Accordion.stories.tsx @@ -7,6 +7,8 @@ import Icon from '../Icon'; import NumberIcon from '../NumberIcon'; import Text from '../Text'; +type Args = React.ComponentProps; + export default { title: 'Components/Accordion', component: Accordion, @@ -36,20 +38,27 @@ export default { }, }, }, - decorators: [(Story) =>
{Story()}
], + decorators: [(Story) =>
{Story()}
], } as Meta; -type Args = React.ComponentProps; +type Story = StoryObj; -export const Default: StoryObj = {}; +export const Default: Story = {}; -export const Small: StoryObj = { +/** + * Default `Accordion` using the `small` size. + */ +export const Small: Story = { args: { size: 'sm', }, }; -export const Stacked: StoryObj = { +/** + * This demonstrates how one can combine multiple `Accordion` rows, where any of the rows can + * be defaulted to open (using `defaultOpen`). + */ +export const Stacked: Story = { args: { children: ( <> @@ -102,21 +111,21 @@ export const Stacked: StoryObj = { }, }; -export const StackedSmall: StoryObj = { +export const StackedSmall: Story = { args: { ...Stacked.args, size: 'sm', }, }; -export const StackedOutline: StoryObj = { +export const StackedOutline: Story = { args: { ...Stacked.args, hasOutline: true, }, }; -export const StackedSmallOutline: StoryObj = { +export const StackedSmallOutline: Story = { args: { ...Stacked.args, size: 'sm', @@ -125,27 +134,9 @@ export const StackedSmallOutline: StoryObj = { }; /** - * The following stories are mostly for visual regression testing to capture the open state. + * This demonstrates how to specify that a section is not currently expandable using `isExpandable`. */ -export const DefaultOpen: StoryObj = { - args: { - children: ( - - - Massa quam egestas massa. - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla amet, - massa ultricies iaculis. Quam lacus maecenas nibh malesuada. At - tristique et ullamcorper rhoncus amet pharetra aliquet tortor. - Suscipit dui, nunc sit dui tellus massa laoreet tellus. - - - ), - }, -}; - -export const EmptyStackedOpen: StoryObj = { +export const EmptyStackedOpen: Story = { args: { children: ( <> @@ -190,14 +181,7 @@ export const EmptyStackedOpen: StoryObj = { }, }; -export const SmallOpen: StoryObj = { - args: { - ...DefaultOpen.args, - size: 'sm', - }, -}; - -export const StackedOpen: StoryObj = { +export const StackedOpen: Story = { args: { children: ( <> @@ -242,21 +226,21 @@ export const StackedOpen: StoryObj = { }, }; -export const StackedSmallOpen: StoryObj = { +export const StackedSmallOpen: Story = { args: { ...StackedOpen.args, size: 'sm', }, }; -export const StackedOutlineOpen: StoryObj = { +export const StackedOutlineOpen: Story = { args: { ...StackedOpen.args, hasOutline: true, }, }; -export const StackedSmallOutlineOpen: StoryObj = { +export const StackedSmallOutlineOpen: Story = { args: { ...StackedOpen.args, size: 'sm', @@ -264,8 +248,13 @@ export const StackedSmallOutlineOpen: StoryObj = { }, }; -// Visual regression testing unhelpful since story value is in interaction and as a code example. -export const UsingRenderProp: StoryObj = { +/** + * + * This shows how to use a render prop for the row, to allow controlling render based on component state. + * + * **NOTE**: Visual regression testing unhelpful since story value is in interaction and as a code example. + */ +export const UsingRenderProp: Story = { render: () => ( @@ -289,7 +278,7 @@ export const UsingRenderProp: StoryObj = { * Although headings should provide limited text, we allow for text to span multiple lines, preserving * the size of the state caret. */ -export const WithLargeHeader: StoryObj = { +export const WithLargeHeader: Story = { parameters: { chromatic: { viewports: [chromaticViewports.ipadMini], @@ -313,7 +302,12 @@ export const WithLargeHeader: StoryObj = { }, }; -export const UsingComplexHeaders: StoryObj = { +/** + * You can use other EDS components within the `Accordion.Button` to allow for custom, non-text headers. + * + * **Example**: using `Text` and `Icon` in the `Accordion.Button`. + */ +export const UsingComplexHeaders: Story = { parameters: { badges: ['1.2', 'implementationExample'], }, @@ -322,7 +316,7 @@ export const UsingComplexHeaders: StoryObj = { <> - + = { - + = { }, }; -export const UsingNumberIconInHeaders: StoryObj = { +/** + * You can use other EDS components within the `Accordion.Button` to allow for custom, non-text headers. + * + * **Example**: using `Text` and `NumberIcon` in the `Accordion.Button`. + */ +export const UsingNumberIconInHeaders: Story = { parameters: { badges: ['1.2', 'implementationExample'], }, @@ -372,11 +371,9 @@ export const UsingNumberIconInHeaders: StoryObj = { <> -
+
- - Step 1 - + Step 1
@@ -388,11 +385,9 @@ export const UsingNumberIconInHeaders: StoryObj = { -
- - - Step 2 - +
+ + Step 2
diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index 7ed22ad0f..35dd30e65 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -18,15 +18,21 @@ type AccordionProps = { */ className?: string; /** - * Outline variant adds extra padding and a rounded border. + * Outline variant adds adjusts the `Accordion` style by defining a containing border and other layout adjustments. + * + * **Default is `false`**. */ hasOutline?: boolean; /** - * Used to specify which heading element should be rendered for each Accordion.Title child. + * Used to specify which heading element should be rendered for each `Accordion.Title` child. + * + * **Default is `"h2"`**. */ headingAs: HeadingElement; /** - * Various Accordion sizes. Defaults to 'md'. + * Various sizes supported by the `Accordion`. + * + * **Default is `"md"`**. */ size?: Extract; }; @@ -41,7 +47,9 @@ type AccordionButtonProps = { */ className?: string; /** - * Icon override for component. Default is 'expand-more' + * Icon override for component. + * + * **Default is `"expand-more"`**. */ icon?: Extract; /** @@ -106,7 +114,7 @@ const AccordionRowContext = createContext<{ isExpandable?: boolean }>({ /** * `import {Accordion} from "@chanzuckerberg/eds;` * - * Displays a list of headers stacked on top of one another that can reveal or hide associated content. + * Displays one or more headers stacked on top of one another that can reveal or hide associated content. * This component is based on the [Disclosure](https://headlessui.com/react/disclosure) component, provided by HeadlessUI. * * @see https://headlessui.com/react/disclosure diff --git a/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap b/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap index d477e91c4..59823a6bc 100644 --- a/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap +++ b/src/components/Accordion/__snapshots__/Accordion.test.tsx.snap @@ -2,7 +2,7 @@ exports[` Default story renders snapshot 1`] = `
Default story renders snapshot 1`] = `
`; -exports[` DefaultOpen story renders snapshot 1`] = ` -
-
-
- -
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla amet, massa ultricies iaculis. Quam lacus maecenas nibh malesuada. At tristique et ullamcorper rhoncus amet pharetra aliquet tortor. Suscipit dui, nunc sit dui tellus massa laoreet tellus. -
-
-
-
-`; - exports[` EmptyStackedOpen story renders snapshot 1`] = `
EmptyStackedOpen story renders snapshot 1`] = ` class="accordion-row" > -
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla amet, massa ultricies iaculis. Quam lacus maecenas nibh malesuada. At tristique et ullamcorper rhoncus amet pharetra aliquet tortor. Suscipit dui, nunc sit dui tellus massa laoreet tellus. -
-
-
-
-`; - exports[` Stacked story renders snapshot 1`] = `
Stacked story renders snapshot 1`] = ` exports[` StackedOpen story renders snapshot 1`] = `
StackedOpen story renders snapshot 1`] = ` class="accordion-row" > -
-
- - - ), -}; - -export const CardWithNotification: StoryObj = { - args: { - elevation: 'raised', - }, - parameters: { - badges: ['1.0', 'implementationExample'], - }, - render: (args) => ( -
- - -
Card Header
-
- -
Card Body
-
- -
Card Footer
-
-
- -
- ), -}; - +/** + * This implementation example shows how you might use `Skeleton` to implement a loading state + * for a recipe-like Profile card. Initially, the component would use the `Skeleton` instances, + * and once the data is fetched, replace those with the actual contents. + */ export const LoadingProfileCard: StoryObj = { args: { isLoading: true, @@ -238,13 +179,14 @@ const InteractiveCard = () => { ); }; +/** + * You can implement appearance changes using `elevation`. In this implementation example, + * we show how to build the basics of a drag/hover behavior using react state, `isDragging`, and `elevation`. + * + * To `Card`, we add handlers on `onMouseDown`, `onMouseEnter`, `onMouseLeave`, and `onMouseUp`. + */ export const InteractiveOnHover: StoryObj = { render: (args) => , -}; - -// Add visual regression test simulating hover -export const StaticOnHover: StoryObj = { - render: (args) => , play: async ({ canvasElement }) => { const canvas = within(canvasElement); const cardContents = await canvas.findByText('Card Contents'); diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index 61464fe6e..1ba6650d0 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -3,7 +3,7 @@ import type { HTMLAttributes, ReactNode } from 'react'; import React from 'react'; import styles from './Card.module.css'; -export interface Props extends HTMLAttributes { +export interface CardProps extends HTMLAttributes { /** * Child node(s) that can be nested inside component */ @@ -17,10 +17,14 @@ export interface Props extends HTMLAttributes { * - **none** renders the card with no elevation (no box-shadow applied) * - **raised** renders the card that is raised off of the canvas (box-shadow applied) * - **dragging** renders the card that is raised even further off the canvas (during drag) + * + * **Default is `'none'`**. */ elevation?: 'none' | 'raised' | 'dragging'; /** - * Property to apply a "dragging" elevation. Used when card is under drag + * Property to apply a "dragging" elevation. Can be use while card is in a moving state. + * + * **Default is `false`**. */ isDragging?: boolean; /** @@ -28,6 +32,9 @@ export interface Props extends HTMLAttributes { * - **vertical** renders the header, body, and footer in a columnar fashion (default) * - **horizontal** renders the header, body, and footer in a horizontal fashion * where the body is required but the header and footer are not + * + * **Default is `"vertical"`**. + * @deprecated */ orientation?: 'vertical' | 'horizontal'; } @@ -47,7 +54,7 @@ export interface CardSubComponentProps { * `import {Card} from "@chanzuckerberg/eds";` * * Card component is the outer wrapper for the block that typically contains a title, image, - * text, and/or calls to action. + * text, and/or calls to action. */ export const Card = ({ className, @@ -56,7 +63,7 @@ export const Card = ({ isDragging = false, orientation = 'vertical', ...other -}: Props) => { +}: CardProps) => { const componentClassName = clsx( styles['card'], orientation === 'horizontal' && styles['card--horizontal'], diff --git a/src/components/Card/CardWithNotification.module.css b/src/components/Card/CardWithNotification.module.css deleted file mode 100644 index 4b1eb4c2d..000000000 --- a/src/components/Card/CardWithNotification.module.css +++ /dev/null @@ -1,24 +0,0 @@ -@import '../../../src/design-tokens/mixins.css'; - -/*------------------------------------*\ - # CARD WITH NOTIFICATION -\*------------------------------------*/ - -/** - * Card with a bottom notification. - */ -.card-with-notification { - /** - * Add a border around the entire card and notification. - */ - border-radius: var(--eds-border-radius-md); - border: var(--eds-theme-color-border-neutral-subtle) solid - var(--eds-theme-border-width); -} -.card-with-notification__card { - /** - * Remove the redundant border that existed on the card component. - */ - border: 0; - border-radius: 0; -} diff --git a/src/components/Card/__snapshots__/Card.test.ts.snap b/src/components/Card/__snapshots__/Card.test.ts.snap index b2cd4c723..b7ad0d88f 100644 --- a/src/components/Card/__snapshots__/Card.test.ts.snap +++ b/src/components/Card/__snapshots__/Card.test.ts.snap @@ -1,106 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` ButtonActionCalloutCard story renders snapshot 1`] = ` -
-
-
-

- Do This Checkpoint -

-
- Develop the text of your Body Book, crafting evidence-supported explanations on how the body is organized and its functions. -
- -
-
-
-
-
-`; - -exports[` CardWithNotification story renders snapshot 1`] = ` -
-
-
-
-
- Card Header -
-
-
-
- Card Body -
-
-
-
- Card Footer -
-
-
-
- - - success - - - - - Lorem ipsum dolor sit amet - -
-
-
-`; - exports[` Default story renders snapshot 1`] = `
InteractiveOnHover story renders snapshot 1`] = ` class="p-8" >
Raised story renders snapshot 1`] = `
`; - -exports[` StaticOnHover story renders snapshot 1`] = ` -
-
-
-
- Card Contents -
-
-
-
-`; diff --git a/src/components/Checkbox/Checkbox.stories.tsx b/src/components/Checkbox/Checkbox.stories.tsx index 9c87845dc..7cacc6920 100644 --- a/src/components/Checkbox/Checkbox.stories.tsx +++ b/src/components/Checkbox/Checkbox.stories.tsx @@ -2,29 +2,18 @@ import type { StoryObj, Meta } from '@storybook/react'; import React from 'react'; import { Checkbox } from './Checkbox'; -const defaultArgs = { - disabled: false, - label: 'Checkbox', -}; - const meta: Meta = { title: 'Components/Checkbox', component: Checkbox, - args: defaultArgs, + args: { + disabled: false, + label: 'Checkbox', + }, parameters: { badges: ['1.0'], }, - decorators: [ - (Story) => ( -
- {Story()} -
- ), - ], + decorators: [(Story) =>
{Story()}
], }; export default meta; @@ -55,60 +44,22 @@ export const MediumChecked: Story = { }, }; -export const Large: Story = { - ...Default, - args: { - size: 'lg', - }, -}; - -export const LargeChecked: Story = { - ...Large, - args: { - ...Checked.args, - ...Large.args, - }, -}; - export const Indeterminate: Story = { args: { indeterminate: true, }, }; +/** + * `Checkbox` can be disabled in each available state. + */ export const Disabled: Story = { - render: () => ( - - - {/* Un-checked */} - - - - - {/* Checked */} - - - - - {/* Indeterminate */} - - - - - -
- - - -
- - - -
- - - -
+ render: (args) => ( +
+ + + +
), parameters: { axe: { @@ -117,35 +68,23 @@ export const Disabled: Story = { }, }; +/** + * `Checkbox` doesn't require a visible label if `aria-label` is provided. + */ export const WithoutVisibleLabel: Story = { args: { 'aria-label': 'a checkbox has no name', label: undefined, }, - render: (args) => ( -
- - - - - - -
- ), }; -export const LongLabels = { - render: () => { - const label = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'; - - return ( -
- - - - -
- ); +/** + * Long labels will sit adjacent to the text box, and allow clicking to change the state of the checkbox. When constrained, + * the text will wrap, fixing the checkbox to the top edge. + */ +export const LongLabels: Story = { + args: { + label: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', }, parameters: { axe: { @@ -153,21 +92,3 @@ export const LongLabels = { }, }, }; - -export const WithCustomPositioning = { - parameters: { - docs: { - source: { - type: 'dynamic', - }, - }, - }, - render: () => ( -
- - Label on Left - - -
- ), -}; diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index e3d2675c7..49ec2afe0 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -3,6 +3,7 @@ import React, { forwardRef } from 'react'; import useForwardedRef from '../../util/useForwardedRef'; import { useId } from '../../util/useId'; import type { EitherInclusive } from '../../util/utility-types'; +import type { Size } from '../../util/variant-types'; import { InputLabel, type InputLabelProps } from '../InputLabel/InputLabel'; import styles from './Checkbox.module.css'; @@ -42,9 +43,11 @@ type CheckboxProps = Omit & { */ id?: string; /** - * Size of the checkbox label. + * Size of the checkbox and associated label. + * + * **Default is `"lg"`**. */ - size?: CheckboxLabelProps['size']; + size?: Extract; } & EitherInclusive< { /** @@ -89,8 +92,6 @@ const CheckboxInput = React.forwardRef( }, ); -CheckboxInput.displayName = 'CheckboxInput'; - const CheckboxLabel = ({ className, size, ...other }: CheckboxLabelProps) => { const componentClassName = clsx( size === 'md' && styles['checkbox__label--md'], @@ -103,9 +104,10 @@ const CheckboxLabel = ({ className, size, ...other }: CheckboxLabelProps) => { /** * `import {Checkbox} from "@chanzuckerberg/eds";` * - * Checkbox control indicating if something is selected or unselected. + * Checkbox control indicating if something is selected or unselected. Uncontrolled by default, + * it can be used in place of boolean-like form data. * - * NOTE: Requires either a visible label or `aria-label` prop. + * **NOTE**: Requires either a visible `label` or `aria-label` prop. */ export const Checkbox = Object.assign( forwardRef((props, ref) => { @@ -138,3 +140,4 @@ export const Checkbox = Object.assign( ); Checkbox.displayName = 'Checkbox'; +CheckboxInput.displayName = 'CheckboxInput'; diff --git a/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap index 4f69389d9..6209f78d2 100644 --- a/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap +++ b/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap @@ -49,128 +49,59 @@ exports[` Disabled story renders snapshot 1`] = `
- - - - - - - - - - - - - - - -
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
+
+ + +
+
+ + +
+
+ + +
+
`; @@ -181,12 +112,12 @@ exports[` Disabled story renders snapshot 2`] = ` @@ -194,28 +125,6 @@ exports[` Disabled story renders snapshot 2`] = ` `; exports[` Indeterminate story renders snapshot 1`] = ` -
-
- - -
-
-`; - -exports[` Large story renders snapshot 1`] = `
@@ -237,7 +146,7 @@ exports[` Large story renders snapshot 1`] = `
`; -exports[` LargeChecked story renders snapshot 1`] = ` +exports[` LongLabels story renders snapshot 1`] = `
@@ -245,96 +154,20 @@ exports[` LargeChecked story renders snapshot 1`] = ` class="checkbox" >
`; -exports[` LongLabels story renders snapshot 1`] = ` -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-`; - exports[` Medium story renders snapshot 1`] = `
MediumChecked story renders snapshot 1`] = `
`; -exports[` WithCustomPositioning story renders snapshot 1`] = ` +exports[` WithoutVisibleLabel story renders snapshot 1`] = `
-
`; - -exports[` WithoutVisibleLabel story renders snapshot 1`] = ` -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-`; diff --git a/src/components/HorizontalStepper/__snapshots__/HorizontalStepper.test.tsx.snap b/src/components/HorizontalStepper/__snapshots__/HorizontalStepper.test.tsx.snap index a289a69b3..ac70d6a51 100644 --- a/src/components/HorizontalStepper/__snapshots__/HorizontalStepper.test.tsx.snap +++ b/src/components/HorizontalStepper/__snapshots__/HorizontalStepper.test.tsx.snap @@ -12,7 +12,7 @@ exports[` CappedLine story renders snapshot 1`] = ` > 1 @@ -32,7 +32,7 @@ exports[` CappedLine story renders snapshot 1`] = ` > CappedLine story renders snapshot 1`] = ` > CappedLine story renders snapshot 1`] = ` > CappedLine story renders snapshot 1`] = ` > HorizontalSteps story renders snapshot 1`] = ` > HorizontalSteps story renders snapshot 1`] = ` > 1 @@ -425,7 +425,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 1 @@ -441,7 +441,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 2 @@ -457,7 +457,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 3 @@ -473,7 +473,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 4 @@ -489,7 +489,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 5 @@ -505,7 +505,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 6 @@ -521,7 +521,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 7 @@ -537,7 +537,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 8 @@ -553,7 +553,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 9 @@ -569,7 +569,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 10 @@ -585,7 +585,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 21 @@ -601,7 +601,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 32 @@ -617,7 +617,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 43 @@ -633,7 +633,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 54 @@ -649,7 +649,7 @@ exports[` HorizontalStepsDifferentNumbers story renders sna > 65 @@ -676,7 +676,7 @@ exports[` OnFirstStep story renders snapshot 1`] = ` > 1 @@ -696,7 +696,7 @@ exports[` OnFirstStep story renders snapshot 1`] = ` > OnFirstStep story renders snapshot 1`] = ` > OnFirstStep story renders snapshot 1`] = ` > OnFirstStep story renders snapshot 1`] = ` > OnLastStep story renders snapshot 1`] = ` > 5 @@ -1054,7 +1054,7 @@ exports[` SomeCompletedSteps story renders snapshot 1`] = ` > 3 @@ -1074,7 +1074,7 @@ exports[` SomeCompletedSteps story renders snapshot 1`] = ` > SomeCompletedSteps story renders snapshot 1`] = ` > = { control: { type: 'select', }, - options: ['currentColor', ...Object.keys(ColorTokens)], + // For now, take the variables and convert to equivalent tokens for the UI + options: [ + 'currentColor', + ...Object.keys(ColorTokens).map( + (tokenVarName) => `var(--${kebabCase(tokenVarName)})`, + ), + ], }, }, }; @@ -34,10 +40,7 @@ type Story = StoryObj; export const Default: Story = { render: ({ name, color, ...rest }) => { - // ESlint can't tell if ColorTokens[color] is valid or not, since it's computed at runtime. - // @ts-expect-error cannot check type compliance on runtime imports - const computedColor = color && ColorTokens[color]; /* eslint-disable-line */ - return ; + return ; }, args: { name: 'close', @@ -61,28 +64,28 @@ export const Large: Story = { }, }; -export const FullScreen: Story = { - ...Default, - args: { - ...Default.args, - size: '100vh', - }, - parameters: { - axe: { - disabledRules: ['scrollable-region-focusable'], - }, - }, -}; - +/** + * You can control the color of the icon using any valid CSS color values, including our token suite. + * + * If `currentColor` for the whole container isn't sufficient, + * use a CSS variable in `color` with the token you need, or + * style `fill` with Tailwind: https://tailwindcss.com/docs/fill + */ export const CustomColor: Story = { ...Default, args: { ...Default.args, - color: 'EdsColorBrandGrape400', + color: 'var(--eds-color-brand-grape-400)', size: '2em', }, }; +/** + * Icons are positioned naturally in lines of text. Use the size, color, or other props + * to match the recommended design and layout. + * + * See: https://material-ui.com/components/material-icons/ + */ export const InText: Story = { render: (args) => { return ( @@ -94,29 +97,17 @@ export const InText: Story = { purpose="informative" title="icon with 1em line height" /> - ; 1em), but often looks better with the line height ( - - ; 2em) which is harder to determine. Take a look at the icons available - in{' '} - - https://material-ui.com/components/material-icons/ - - , currently we only support the filled icons. + ; 1em) by default. ); }, }; +/** + * If your product needs icons not currently existing in the suite, you can introduce new + * accessible icons by inserting the body of an SVG into `Icon`. Each resulting icon can be + * treated like a standalone component, matching the recipe defined by design. + */ export const WithChildrenSvg: Story = { ...Default, args: { @@ -127,72 +118,20 @@ export const WithChildrenSvg: Story = { }, }; -/** - * Grid of all the available icons. - */ -const IconsInGrid = () => ( +const IconsInGrid = (args: IconProps) => (
    {(Object.keys(icons) as IconName[]).map((name) => { return (
  • - + {name} - {name === 'warning' && ( -
    - - This has been replaced by status-warning. This will be - deprecated -
    - )} - {name === 'check-circle' && ( -
    - - This has been replaced by status-check-circle. This will be - deprecated -
    - )} - {name === 'info' && ( -
    - - This has been replaced by status-info. This will be deprecated -
    - )} - {(name === 'error' || name === 'error-inverted') && ( -
    - - This has been replaced by status-error. This will be deprecated -
    - )} {name === 'avatar' && (
    This has been replaced by person. This will be deprecated
    @@ -202,7 +141,7 @@ const IconsInGrid = () => ( This has been replaced by book. This will be deprecated
@@ -212,7 +151,7 @@ const IconsInGrid = () => ( This has been replaced by copy. This will be deprecated
@@ -222,11 +161,27 @@ const IconsInGrid = () => ( This has been replaced by dots-vertical. This will be deprecated
)} + {[ + 'status-check-circle', + 'status-error', + 'status-info', + 'status-warning', + ].includes(name) && ( +
+ + Icons with baked-in colors are not recommended. This will be + deprecated. +
+ )} ); })} @@ -234,6 +189,11 @@ const IconsInGrid = () => ( ); -export const IconGrid: StoryObj = { - render: () => , +/** + * Grid of all the available icons. Use the controls to change color, or other attributes + * + * **NOTE**: some icons marked as deprecated will be removed in future releases. + */ +export const IconGrid: Story = { + render: (args) => , }; diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index cabe657c1..a3de1556f 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -28,10 +28,8 @@ interface IconPropsBase { */ children?: ReactNode; /** - * The SVG Color, expects a valid css color (hex, rgb, etc.). + * The SVG Color, expects a valid css color (hex, rgb, css variable, etc.). * - * Recommendation: if `currentColor` isn't sufficient, - * style the fill with Tailwind: https://tailwindcss.com/docs/fill */ color?: string; /** @@ -91,13 +89,8 @@ interface SvgStyle extends CSSProperties { * * Render arbitrary SVG path data while enforcing good accessibility practices. * - * If you're looking for specific icon files, look in the `src/icons` directory. - * - * Example usage: - * - * ``` - * - * ``` + * Icons are based on [Material Rounded](https://fonts.google.com/icons?icon.set=Material+Icons&icon.style=Rounded), + * and are encoded in a spritemap in `src/icons`. */ export const Icon = (props: IconProps) => { const { diff --git a/src/components/Icon/__snapshots__/Icon.test.ts.snap b/src/components/Icon/__snapshots__/Icon.test.ts.snap index 3c34ff494..ece16ff2a 100644 --- a/src/components/Icon/__snapshots__/Icon.test.ts.snap +++ b/src/components/Icon/__snapshots__/Icon.test.ts.snap @@ -4,7 +4,7 @@ exports[` CustomColor story renders snapshot 1`] = ` Default story renders snapshot 1`] = ` `; -exports[` FullScreen story renders snapshot 1`] = ` - -`; - exports[` IconGrid story renders snapshot 1`] = `
    IconGrid story renders snapshot 1`] = ` > check-circle -
    - - This has been replaced by status-check-circle. This will be deprecated -
  • IconGrid story renders snapshot 1`] = ` > error-inverted -
    - - This has been replaced by status-error. This will be deprecated -
  • IconGrid story renders snapshot 1`] = ` > error -
    - - This has been replaced by status-error. This will be deprecated -
  • IconGrid story renders snapshot 1`] = ` > info -
    - - This has been replaced by status-info. This will be deprecated -
  • IconGrid story renders snapshot 1`] = ` > status-check-circle +
    + + Icons with baked-in colors are not recommended. This will be deprecated. +
  • IconGrid story renders snapshot 1`] = ` > status-error +
    + + Icons with baked-in colors are not recommended. This will be deprecated. +
  • IconGrid story renders snapshot 1`] = ` > status-info +
    + + Icons with baked-in colors are not recommended. This will be deprecated. +
  • IconGrid story renders snapshot 1`] = ` > status-warning +
    + + Icons with baked-in colors are not recommended. This will be deprecated. +
  • IconGrid story renders snapshot 1`] = ` > warning -
    - - This has been replaced by status-warning. This will be deprecated -
@@ -2876,35 +2825,7 @@ exports[` InText story renders snapshot 1`] = ` d="M5.85 17.1C6.7 16.45 7.65 15.9375 8.7 15.5625C9.75 15.1875 10.85 15 12 15C13.15 15 14.25 15.1875 15.3 15.5625C16.35 15.9375 17.3 16.45 18.15 17.1C18.7333 16.4167 19.1875 15.6417 19.5125 14.775C19.8375 13.9083 20 12.9833 20 12C20 9.78333 19.2208 7.89583 17.6625 6.3375C16.1042 4.77917 14.2167 4 12 4C9.78333 4 7.89583 4.77917 6.3375 6.3375C4.77917 7.89583 4 9.78333 4 12C4 12.9833 4.1625 13.9083 4.4875 14.775C4.8125 15.6417 5.26667 16.4167 5.85 17.1ZM12 13C11.0167 13 10.1875 12.6625 9.5125 11.9875C8.8375 11.3125 8.5 10.4833 8.5 9.5C8.5 8.51667 8.8375 7.6875 9.5125 7.0125C10.1875 6.3375 11.0167 6 12 6C12.9833 6 13.8125 6.3375 14.4875 7.0125C15.1625 7.6875 15.5 8.51667 15.5 9.5C15.5 10.4833 15.1625 11.3125 14.4875 11.9875C13.8125 12.6625 12.9833 13 12 13ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22ZM12 20C12.8833 20 13.7167 19.8708 14.5 19.6125C15.2833 19.3542 16 18.9833 16.65 18.5C16 18.0167 15.2833 17.6458 14.5 17.3875C13.7167 17.1292 12.8833 17 12 17C11.1167 17 10.2833 17.1292 9.5 17.3875C8.71667 17.6458 8 18.0167 7.35 18.5C8 18.9833 8.71667 19.3542 9.5 19.6125C10.2833 19.8708 11.1167 20 12 20ZM12 11C12.4333 11 12.7917 10.8583 13.075 10.575C13.3583 10.2917 13.5 9.93333 13.5 9.5C13.5 9.06667 13.3583 8.70833 13.075 8.425C12.7917 8.14167 12.4333 8 12 8C11.5667 8 11.2083 8.14167 10.925 8.425C10.6417 8.70833 10.5 9.06667 10.5 9.5C10.5 9.93333 10.6417 10.2917 10.925 10.575C11.2083 10.8583 11.5667 11 12 11Z" /> - ; 1em), but often looks better with the line height ( - - - icon with 2em line height - - - - ; 2em) which is harder to determine. Take a look at the icons available in - - - https://material-ui.com/components/material-icons/ - - , currently we only support the filled icons. + ; 1em) by default.

`; diff --git a/src/components/InputField/InputField.stories.tsx b/src/components/InputField/InputField.stories.tsx index fd4a684ea..d63001e3f 100644 --- a/src/components/InputField/InputField.stories.tsx +++ b/src/components/InputField/InputField.stories.tsx @@ -11,14 +11,11 @@ const meta: Meta = { component: InputField, parameters: { badges: ['1.0'], + backgrounds: { + default: 'eds-color-neutral-white', + }, }, - decorators: [ - (Story) => ( -
- {Story()} -
- ), - ], + decorators: [(Story) =>
{Story()}
], }; export default meta; @@ -75,6 +72,11 @@ export const NoVisibleLabel: Story = { }, }; +/** + * You can render certain components **within** an `InputField`, such as a button, icon, or other + * small component. This facility is used to implement controls that should appear visibly nested + * within the button, to the right-hand side. + */ export const InputWithin: Story = { parameters: { chromatic: { disableSnapshot: true }, @@ -97,149 +99,11 @@ export const InputWithin: Story = { ), }; -export const LabelFieldnoteVariants: Story = { - render: (args) => ( -
-
-

Placeholder

-

No Placeholder

- -

fieldNote, label

- - -

no fieldNote, label

- - -

fieldNote, no label

- - -

no fieldNote, no label

- - -
- ), -}; - -export const ErrorVariants: Story = { - args: { - isError: true, - }, - ...LabelFieldnoteVariants, -}; - -export const DisabledVariants: Story = { - args: { - disabled: true, - }, - ...LabelFieldnoteVariants, - parameters: { - axe: { - // Disabled input does not need to meet color contrast - disabledRules: ['color-contrast'], - }, - }, -}; - -export const RequiredVariants: Story = { - args: { - required: true, - }, - render: (args) => ( -
-
-

Placeholder

-

No Placeholder

- -

fieldNote, label

- - -

no fieldNote, label

- - -

fieldNote, no label

- - -

no fieldNote, no label

- - -

fieldNote, label, isError

- - -

fieldNote, label, disabled

- - -
- ), - parameters: { - axe: { - // Disabled input does not need to meet color contrast - disabledRules: ['color-contrast'], - }, - }, -}; - +/** + * You can lock the maximum length of the text content of `InputField`. When setting `maxLength`, + * the field will reuse the browser's [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + * behavior (e.g., prevent further text from being typed, prevent keydown events, etc.). + */ export const WithAMaxLength: Story = { args: { defaultValue: 'Some initial text', @@ -250,6 +114,11 @@ export const WithAMaxLength: Story = { render: (args) => , }; +/** + * If you want to signal that a field has reached a maximum length but want to allow more text to be typed, you can use + * `recommendedMaxLength`. This will show a similar UI to using `maxLength` but will allow more text to be typed, and + * emit any appropriate events. + */ export const WithARecommendedLength: Story = { args: { defaultValue: 'Some initial text', @@ -260,6 +129,10 @@ export const WithARecommendedLength: Story = { render: (args) => , }; +/** + * Both `maxLength` and `recommendedMaxLength` can be specified at the same time. Text length between `recommendedMaxLength` + * and `maxLength` will show the treatment warning the user about the text length being violated. + */ export const WithBothMaxAndRecommendedLength: Story = { args: { label: 'test label', @@ -272,6 +145,10 @@ export const WithBothMaxAndRecommendedLength: Story = { render: (args) => , }; +/** + * This **implementation example** shows how to attach labels to fields and labels + * when the label is in a different column. + */ export const TabularInput: Story = { parameters: { badges: ['1.1', 'implementationExample'], diff --git a/src/components/InputField/InputField.tsx b/src/components/InputField/InputField.tsx index 7d5f75fca..2c2de69da 100644 --- a/src/components/InputField/InputField.tsx +++ b/src/components/InputField/InputField.tsx @@ -13,7 +13,7 @@ import InputLabel from '../InputLabel'; import Text from '../Text'; import styles from './InputField.module.css'; -export type Props = React.InputHTMLAttributes & { +export type InputFieldProps = React.InputHTMLAttributes & { /** * CSS class names that can be appended to the component. */ @@ -31,23 +31,17 @@ export type Props = React.InputHTMLAttributes & { */ id?: string; /** - * Gives a hint as to the type of data needed for text input + * Gives a hint as to the type of data needed for text input (e.g., to render the correct input keyboard on mobile). */ - inputMode?: - | 'text' - | 'email' - | 'url' - | 'search' - | 'tel' - | 'none' - | 'numeric' - | 'decimal'; + inputMode?: React.InputHTMLAttributes['inputMode']; /** * Node(s) that can be nested within the input field */ inputWithin?: ReactNode; /** - * Error state of the form field + * Error variant of the form field + * + * **Default is `false`**. */ isError?: boolean; /** @@ -93,8 +87,11 @@ export type Props = React.InputHTMLAttributes & { title?: string; /** * HTML type attribute, allowing switching between text, password, and other HTML5 input field types + * + * **NOTE**: this excludes types that correspond to non-text controls, like `checkbox`, `radio`, etc. */ - type?: + type?: Extract< + React.InputHTMLAttributes['type'], | 'text' | 'password' | 'datetime' @@ -107,7 +104,8 @@ export type Props = React.InputHTMLAttributes & { | 'email' | 'url' | 'search' - | 'tel'; + | 'tel' + >; /** * Value passed down from higher levels for initial state */ @@ -131,7 +129,10 @@ export type Props = React.InputHTMLAttributes & { } >; -type InputFieldType = ForwardedRefComponent & { +type InputFieldType = ForwardedRefComponent< + HTMLInputElement, + InputFieldProps +> & { Input?: typeof Input; Label?: typeof InputLabel; }; @@ -141,7 +142,7 @@ type InputFieldType = ForwardedRefComponent & { * * An input with optional labels and error messaging built-in. * - * NOTE: This component requires `label` or `aria-label` prop + * **NOTE**: This component requires `label` or `aria-label` prop */ export const InputField: InputFieldType = forwardRef( ( diff --git a/src/components/InputField/__snapshots__/InputField.test.tsx.snap b/src/components/InputField/__snapshots__/InputField.test.tsx.snap index 20cee0ecf..8e74424c1 100644 --- a/src/components/InputField/__snapshots__/InputField.test.tsx.snap +++ b/src/components/InputField/__snapshots__/InputField.test.tsx.snap @@ -2,7 +2,7 @@ exports[` Default story renders snapshot 1`] = `
Default story renders snapshot 1`] = ` exports[` Disabled story renders snapshot 1`] = `
Disabled story renders snapshot 1`] = `
`; -exports[` DisabledVariants story renders snapshot 1`] = ` -
-
-
-

- Placeholder -

-

- No Placeholder -

-

- fieldNote, label -

-
-
- -
-
- -
-
- fieldNote text -
-
-
-
- -
-
- -
-
- fieldNote text -
-
-

- no fieldNote, label -

-
-
- -
-
- -
-
-
-
- -
-
- -
-
-

- fieldNote, no label -

-
-
- -
-
- fieldNote text -
-
-
-
- -
-
- fieldNote text -
-
-

- no fieldNote, no label -

-
-
- -
-
-
-
- -
-
-
-
-`; - exports[` Error story renders snapshot 1`] = `
Error story renders snapshot 1`] = `
`; -exports[` ErrorVariants story renders snapshot 1`] = ` -
-
-
-

- Placeholder -

-

- No Placeholder -

-

- fieldNote, label -

-
-
- -
-
- -
-
- - - error - - - - fieldNote text -
-
-
-
- -
-
- -
-
- - - error - - - - fieldNote text -
-
-

- no fieldNote, label -

-
-
- -
-
- -
-
-
-
- -
-
- -
-
-

- fieldNote, no label -

-
-
- -
-
- - - error - - - - fieldNote text -
-
-
-
- -
-
- - - error - - - - fieldNote text -
-
-

- no fieldNote, no label -

-
-
- -
-
-
-
- -
-
-
-
-`; - exports[` InputWithin story renders snapshot 1`] = `
InputWithin story renders snapshot 1`] = `
`; -exports[` LabelFieldnoteVariants story renders snapshot 1`] = ` -
-
-
-

- Placeholder -

-

- No Placeholder -

-

- fieldNote, label -

-
-
- -
-
- -
-
- fieldNote text -
-
-
-
- -
-
- -
-
- fieldNote text -
-
-

- no fieldNote, label -

-
-
- -
-
- -
-
-
-
- -
-
- -
-
-

- fieldNote, no label -

-
-
- -
-
- fieldNote text -
-
-
-
- -
-
- fieldNote text -
-
-

- no fieldNote, no label -

-
-
- -
-
-
-
- -
-
-
-
-`; - exports[` NoFieldnote story renders snapshot 1`] = `
NoFieldnote story renders snapshot 1`] = ` exports[` NoVisibleLabel story renders snapshot 1`] = `
NoVisibleLabel story renders snapshot 1`] = ` exports[` Required story renders snapshot 1`] = `
Required story renders snapshot 1`] = `
`; -exports[` RequiredVariants story renders snapshot 1`] = ` -
-
-
-

- Placeholder -

-

- No Placeholder -

-

- fieldNote, label -

-
-
- - - Required - -
-
- -
-
- fieldNote text -
-
-
-
- - - Required - -
-
- -
-
- fieldNote text -
-
-

- no fieldNote, label -

-
-
- - - Required - -
-
- -
-
-
-
- - - Required - -
-
- -
-
-

- fieldNote, no label -

-
-
- - Required - -
-
- -
-
- fieldNote text -
-
-
-
- - Required - -
-
- -
-
- fieldNote text -
-
-

- no fieldNote, no label -

-
-
- - Required - -
-
- -
-
-
-
- - Required - -
-
- -
-
-

- fieldNote, label, isError -

-
-
- - - Required - -
-
- -
-
- - - error - - - - fieldNote text -
-
-
-
- - - Required - -
-
- -
-
- - - error - - - - fieldNote text -
-
-

- fieldNote, label, disabled -

-
-
- - - Required - -
-
- -
-
- fieldNote text -
-
-
-
- - - Required - -
-
- -
-
- fieldNote text -
-
-
-
-`; - exports[` TabularInput story renders snapshot 1`] = `
TabularInput story renders snapshot 1`] = ` exports[` WithAMaxLength story renders snapshot 1`] = `
WithAMaxLength story renders snapshot 1`] = ` > @@ -1495,7 +363,7 @@ exports[` WithAMaxLength story renders snapshot 1`] = ` WithAMaxLength story renders snapshot 1`] = ` exports[` WithARecommendedLength story renders snapshot 1`] = `
WithARecommendedLength story renders snapshot 1`] = ` > @@ -1548,7 +416,7 @@ exports[` WithARecommendedLength story renders snapshot 1`] = ` WithARecommendedLength story renders snapshot 1`] = ` exports[` WithBothMaxAndRecommendedLength story renders snapshot 1`] = `
WithBothMaxAndRecommendedLength story renders snapshot 1 > @@ -1598,10 +466,10 @@ exports[` WithBothMaxAndRecommendedLength story renders snapshot 1 class="input-field__body input-field--has-fieldNote" > WithBothMaxAndRecommendedLength story renders snapshot 1 >
; /** * Indicates disabled state of the input. */ diff --git a/src/components/Modal/Modal.stories.module.css b/src/components/Modal/Modal.stories.module.css deleted file mode 100644 index 85b63ab26..000000000 --- a/src/components/Modal/Modal.stories.module.css +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Footer button group that is justified toward the end of the footer. - */ -.footer__button-group { - display: flex; - width: 100%; - justify-content: flex-end; -} - -/** - * Wraps the non interactive stories. - */ -.default__wrapper { - display: flex; - align-items: center; - justify-content: center; -} - -/** - * Background for the non interactive stories. - */ -.default__background { - position: absolute; - height: 100%; - width: 100%; - background-color: var(--eds-theme-color-body-background-inverted); - opacity: 0.5; -} - -/** - * Last paragraph of the long text stories. - * Removes browser placed margin-bottom for

. - */ -.long-text__last-paragraph { - margin-bottom: 0; -} - -/** - * Footer story example with stepper. Separates the stepper and button group. - */ -.footer--with-stepper { - display: flex; - justify-content: space-between; - align-items: center; -} diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index 9fee1c9aa..8db7c3ba0 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -4,10 +4,9 @@ import type { ReactNode } from 'react'; import React from 'react'; import { useState } from 'react'; import { Modal, ModalContent } from './Modal'; -import { Button, ButtonGroup, Heading, Text, Tooltip } from '../../'; +import { Button, ButtonGroup, Heading, Tooltip } from '../../'; import { chromaticViewports, storybookViewports } from '../../util/viewports'; import { VARIANTS } from '../Heading/Heading'; -import styles from './Modal.stories.module.css'; export default { title: 'Components/Modal', @@ -18,16 +17,35 @@ export default { chromatic: { disableSnapshot: true }, badges: ['1.0'], }, + tags: ['autodocs'], argTypes: { + // For some reason, storybook is not able to pick up the doc.s automatically. Adding manually. children: { control: { type: null, }, + description: + 'Contains the sub-components for a Modal, including `.Header` , `.Title` , `.Body` , `.Footer` , `.Stepper`', + }, + open: { + type: 'boolean', + description: 'Whether or not the modal is visible.', + }, + hideCloseButton: { + description: + 'Hides the close button in the top right of the modal. **Default is `false`**.', + type: 'boolean', + }, + isScrollable: { + description: + 'Toggles scrollable variant of the modal. If modal is scrollable, footer is not, and vice versa.', + type: 'boolean', }, }, -} as Meta; +} as Meta; type Args = React.ComponentProps; +type Story = StoryObj; type InteractiveArgs = Omit; const getChildren = ( @@ -37,11 +55,7 @@ const getChildren = ( ) => ( <> - Brand Asset -

- } + brandAsset={
Brand Asset
} > {inDialogComponent ? ( Modal Title @@ -52,168 +66,29 @@ const getChildren = ( {bodyContent} {showStepper && } - + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + - - ); -export const Default: StoryObj = { - render: (args) => ( -
-
- {}} - /> -
- ), - args: { - children: getChildren(false), - hideCloseButton: false, - open: true, - }, - parameters: { - // This story shows the modal content by default, for visual regression testing purposes. - chromatic: { disableSnapshot: false }, - }, -}; - -export const Medium: StoryObj = { - ...Default, - args: { - ...Default.args, - size: 'md', - }, -}; - -export const Small: StoryObj = { - ...Default, - args: { - ...Default.args, - size: 'sm', - }, -}; - -export const Brand: StoryObj = { - ...Default, - args: { - ...Default.args, - variant: 'brand', - }, -}; - -export const Mobile: StoryObj = { - ...Default, - parameters: { - ...Default.parameters, - viewport: { - defaultViewport: 'googlePixel2', - }, - chromatic: { - disableSnapshot: false, - viewports: [chromaticViewports.googlePixel2], - }, - }, -}; - -export const MobileLandscape: StoryObj = { - ...Default, - parameters: { - ...Default.parameters, - viewport: { - defaultViewport: 'mobilelandscape', - viewports: { - mobilelandscape: { - name: 'Mobile Landscape', - styles: { - width: '896px', - height: '414px', - }, - }, - }, - /** - * Chromatic sets viewport height to 900px, hence won't snap as necessary - */ - chromatic: { disableSnapshot: true }, - }, - }, -}; - -export const MobileBrand: StoryObj = { - ...Mobile, - args: { - ...Mobile.args, - variant: 'brand', - }, -}; - -export const MobileLandscapeBrand: StoryObj = { - ...MobileLandscape, - args: { - ...MobileLandscape.args, - variant: 'brand', - }, -}; - -export const Tablet: StoryObj = { - ...Default, - parameters: { - ...Default.parameters, - viewport: { - defaultViewport: 'ipadMini', - viewports: { - mobilelandscape: storybookViewports.ipadMini, - }, - }, - chromatic: { - disableSnapshot: false, - viewports: [chromaticViewports.ipadMini], - }, - }, -}; - -export const TabletBrand: StoryObj = { - ...Tablet, - args: { - ...Tablet.args, - variant: 'brand', - }, -}; - -export const WithStepper: StoryObj = { - ...Default, - args: { - ...Default.args, - children: getChildren( - false, - 'Modal body content. This is an example use case with the stepper in the footer.', - true, - ), - }, -}; - function InteractiveExample(args: InteractiveArgs) { const [open, setOpen] = useState(false); return ( <> - - Please note: opening the modal only works in the Canvas tab. - @@ -223,13 +98,25 @@ function InteractiveExample(args: InteractiveArgs) { ); } -export const DefaultInteractive: StoryObj = { +/** + * Clicking on a trigger item (in this case `Button`) will cause a modal to open. + * + * **Note**: this only works from certain screens in Storybook. If it doesn't work as expected, view from the + * "docs" sub-page. + */ +export const Default: StoryObj = { render: (args) => ( {getChildren()} ), }; type HeadingArgs = React.ComponentProps; +/** + * Clicking on a trigger item (in this case `Button`) will cause a modal to open. + * + * **Note**: this only works from certain screens in Storybook. If it doesn't work as expected, view from the + * "docs" sub-page. + */ export const ControlHeadingInteractive: StoryObj = { argTypes: { as: { @@ -240,6 +127,7 @@ export const ControlHeadingInteractive: StoryObj = { size: { control: 'select', name: 'title "size" prop', + // TODO: convert to using `presets` from variant-types and updating modal similar to `Text` options: [ 'h1', 'h2', @@ -283,6 +171,11 @@ export const ControlHeadingInteractive: StoryObj = { }, }; +/** + * You can disable the close button on the modal. This will require users to either click out of the modal, or hit escape to close. + * + * **NOTE**: this is less discoverable and should be avoided when possible. + */ export const WithoutCloseButton: StoryObj = { render: (args) => ( @@ -341,7 +234,7 @@ const reallyLongText = ( sapien. Nam in egestas tellus. Nulla quis metus dui. Suspendisse sit amet nisi at lectus ultricies egestas.

-

+

Integer pulvinar felis sit amet dignissim fermentum. Nulla sodales enim mi, varius feugiat sapien congue eget. Morbi vitae ipsum non ligula eleifend molestie. Aenean bibendum tortor sapien, quis volutpat ante @@ -355,14 +248,9 @@ const reallyLongText = (

); -export const WithLongText: StoryObj = { - render: (args) => ( - - {getChildren(true, reallyLongText)} - - ), -}; - +/** + * Modals can contain long, scrollable text. This is not recommended, however. + */ export const WithLongTextScrollable: StoryObj = { args: { isScrollable: true, @@ -374,6 +262,9 @@ export const WithLongTextScrollable: StoryObj = { ), }; +/** + * You can avoid rendering the header and footer in a modal + */ export const WithoutHeaderAndFooter: StoryObj = { args: { isScrollable: true, @@ -388,64 +279,171 @@ export const WithoutHeaderAndFooter: StoryObj = { ), }; -type ModalStepperArgs = React.ComponentProps; - -export const ModalStepper: StoryObj = { +export const ModalStepper: StoryObj< + React.ComponentProps +> = { args: { activeStep: 1, totalSteps: 3, }, render: (args) => , - decorators: [ - (Story) => ( -
- {Story()} -
- ), - ], + decorators: [(Story) =>
{Story()}
], parameters: { chromatic: { disableSnapshot: false }, }, }; - -const InteractiveModalStepperComponent = () => { - const [activeStep, setActiveStep] = useState(1); - return ( -
- - - {activeStep > 1 && ( - - )} - {activeStep < 5 && ( - - )} - +/** + * The default modal experience's Content area (the modal itself). The following stories show how the modal + * will render in various contexts and with various props set. + * + * **NOTE**: The order of the buttons puts the primary to the far bottom and right of the modal, per + * macOS conventions and style guide. + */ +export const ContentDefault: Story = { + render: (args) => ( +
+
+ {}} + />
- ); + ), + args: { + children: getChildren(false), + hideCloseButton: false, + open: true, + }, + parameters: { + // This story shows the modal content by default, for visual regression testing purposes. + chromatic: { disableSnapshot: false }, + }, +}; + +/** + * `Modal` provides `size`, which allows control over the natural width of the modal. This does not affect the contents + * of the modal. + */ +export const Medium: Story = { + ...ContentDefault, + args: { + ...ContentDefault.args, + size: 'md', + }, }; -export const InteractiveModalStepper: StoryObj = { - ...ModalStepper, - render: () => , - /** - * For interactive purposes only, low value in snapping or checking for visual regression since they should be covered in the other stories. - */ +/** + * `Modal` also allows for `small`. + */ +export const Small: Story = { + ...ContentDefault, + args: { + ...ContentDefault.args, + size: 'sm', + }, +}; + +/** + * The brand variant offers a catered experience with a slot for a hero image to be attached to the modal. + */ +export const Brand: Story = { + ...ContentDefault, + args: { + ...ContentDefault.args, + variant: 'brand', + }, +}; + +export const Mobile: Story = { + ...ContentDefault, parameters: { - snapshot: { skip: true }, + ...ContentDefault.parameters, + viewport: { + defaultViewport: 'googlePixel2', + }, + chromatic: { + disableSnapshot: false, + viewports: [chromaticViewports.googlePixel2], + }, + }, +}; + +export const MobileLandscape: Story = { + ...ContentDefault, + parameters: { + ...ContentDefault.parameters, + viewport: { + defaultViewport: 'mobilelandscape', + viewports: { + mobilelandscape: { + name: 'Mobile Landscape', + styles: { + width: '896px', + height: '414px', + }, + }, + }, + /** + * Chromatic sets viewport height to 900px, hence won't snap as necessary + */ + chromatic: { disableSnapshot: true }, + }, + }, +}; + +export const MobileBrand: Story = { + ...Mobile, + args: { + ...Mobile.args, + variant: 'brand', + }, +}; + +export const MobileLandscapeBrand: Story = { + ...MobileLandscape, + args: { + ...MobileLandscape.args, + variant: 'brand', + }, +}; + +export const Tablet: Story = { + ...ContentDefault, + parameters: { + ...ContentDefault.parameters, + viewport: { + defaultViewport: 'ipadMini', + viewports: { + mobilelandscape: storybookViewports.ipadMini, + }, + }, + chromatic: { + disableSnapshot: false, + viewports: [chromaticViewports.ipadMini], + }, + }, +}; + +export const TabletBrand: Story = { + ...Tablet, + args: { + ...Tablet.args, + variant: 'brand', + }, +}; + +/** + * `Modal` can make use of the embedded `.Stepper` sub-component, to implement wizard behavior. + */ +export const WithStepper: Story = { + ...ContentDefault, + args: { + ...ContentDefault.args, + children: getChildren( + false, + 'Modal body content. This is an example use case with the stepper in the footer.', + true, + ), }, }; diff --git a/src/components/Modal/Modal.test.tsx b/src/components/Modal/Modal.test.tsx index c4317f204..c4e762b54 100644 --- a/src/components/Modal/Modal.test.tsx +++ b/src/components/Modal/Modal.test.tsx @@ -8,7 +8,7 @@ import { Modal } from './Modal'; import * as stories from './Modal.stories'; import '../../../jest/helpers/removeModalTransitionStylesJestSerializer'; -const { DefaultInteractive } = composeStories(stories); +const { Default } = composeStories(stories); window.ResizeObserver = class FakeResizeObserver { observe() {} @@ -37,13 +37,13 @@ describe('Modal', () => { }); it('is initially closed', () => { - render(); + render(); expect(screen.queryByRole('dialog')).toBeFalsy(); }); it('shows the modal when the open modal button is clicked', async () => { const user = userEvent.setup(); - render(); + render(); const openModalButton = await screen.findByRole('button', { name: 'Open the modal', }); @@ -54,7 +54,7 @@ describe('Modal', () => { it('closes the modal on close button click', async () => { const user = userEvent.setup(); - render(); + render(); const openModalButton = await screen.findByRole('button', { name: 'Open the modal', }); @@ -70,7 +70,7 @@ describe('Modal', () => { it('closes the modal on ESC key press', async () => { const user = userEvent.setup(); - render(); + render(); const openModalButton = await screen.findByRole('button', { name: 'Open the modal', }); diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 0a3c74a15..d9de1c9d2 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -3,6 +3,7 @@ import clsx from 'clsx'; import type { MutableRefObject, ReactNode } from 'react'; import React from 'react'; import type { ExtractProps } from '../../util/utility-types'; +import type { Size } from '../../util/variant-types'; import type { HeadingSize } from '../Heading'; import Heading from '../Heading'; import { Icon, type IconName } from '../Icon/Icon'; @@ -28,6 +29,8 @@ type ModalContentProps = { children: ReactNode; /** * Hides the close button in the top right of the modal. + * + * **Default is `false`**. */ hideCloseButton?: boolean; /** @@ -51,7 +54,8 @@ type ModalContentProps = { * Prop should be dependent on whether content overflows at the mobile level. * Tabindex for keyboard scroll is on the body, however, due to focus outline * not having high contrast on the brand header and being overlapped by the footer. - * Defaults to false since modal default is not scrollable. + * + * **Default is `false`**. */ isScrollable?: boolean; /** @@ -74,8 +78,10 @@ type ModalContentProps = { /** * Max size of the modal. Defaults to 'lg'. * Will still break responsively. + * + * **Default is `"lg"`**. */ - size?: 'sm' | 'md' | 'lg'; + size?: Extract; /** * Color variants of the modal. */ @@ -406,6 +412,7 @@ const ModalHeader = ({ /** * Stepper for the modal to indicate page status. + * TODO: Separate the stepper from the modal, and make into a standalone component. */ const ModalStepper = ({ activeStep, diff --git a/src/components/Modal/__snapshots__/Modal.test.tsx.snap b/src/components/Modal/__snapshots__/Modal.test.tsx.snap index 4f4b83cef..2a155a81c 100644 --- a/src/components/Modal/__snapshots__/Modal.test.tsx.snap +++ b/src/components/Modal/__snapshots__/Modal.test.tsx.snap @@ -39,8 +39,7 @@ exports[`Modal Brand story renders snapshot 1`] = ` class="modal-header__brand-asset" >
Brand Asset
@@ -55,26 +54,26 @@ exports[`Modal Brand story renders snapshot 1`] = ` class="modal-footer" >
`; -exports[`Modal Default story renders snapshot 1`] = ` +exports[`Modal ContentDefault story renders snapshot 1`] = `
`; -exports[`Modal DefaultInteractive story renders snapshot 1`] = ` +exports[`Modal Default story renders snapshot 1`] = ` @@ -262,19 +261,19 @@ exports[`Modal Medium story renders snapshot 1`] = ` class="modal-footer" >
@@ -326,19 +325,19 @@ exports[`Modal Mobile story renders snapshot 1`] = ` class="modal-footer" >
@@ -384,8 +383,7 @@ exports[`Modal MobileBrand story renders snapshot 1`] = ` class="modal-header__brand-asset" >
Brand Asset
@@ -400,19 +398,19 @@ exports[`Modal MobileBrand story renders snapshot 1`] = ` class="modal-footer" >
@@ -464,19 +462,19 @@ exports[`Modal MobileLandscape story renders snapshot 1`] = ` class="modal-footer" >
@@ -522,8 +520,7 @@ exports[`Modal MobileLandscapeBrand story renders snapshot 1`] = ` class="modal-header__brand-asset" >
Brand Asset
@@ -538,19 +535,19 @@ exports[`Modal MobileLandscapeBrand story renders snapshot 1`] = ` class="modal-footer" >
@@ -677,19 +674,19 @@ exports[`Modal Small story renders snapshot 1`] = ` class="modal-footer" >
@@ -741,19 +738,19 @@ exports[`Modal Tablet story renders snapshot 1`] = ` class="modal-footer" >
@@ -799,8 +796,7 @@ exports[`Modal TabletBrand story renders snapshot 1`] = ` class="modal-header__brand-asset" >
Brand Asset
@@ -815,26 +811,26 @@ exports[`Modal TabletBrand story renders snapshot 1`] = ` class="modal-footer" >
`; -exports[`Modal WithLongText story renders snapshot 1`] = ` +exports[`Modal WithLongTextScrollable story renders snapshot 1`] = `
- -
-`; - -exports[`Modal WithLongTextScrollable story renders snapshot 1`] = ` - @@ -1279,14 +1177,14 @@ exports[`Modal WithoutHeaderAndFooter story renders snapshot 1`] = ` aria-modal="true" class="modal modal__transition--enterTo" data-headlessui-state="open" - id="headlessui-dialog-:rc:" + id="headlessui-dialog-:r9:" role="dialog" >
AlignTableCellContentCenter story renders snapshot 1`] = ` -
Default story renders snapshot 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Status + + Chart + + Window + + Last Update +
+ Table Row 1, Table Cell 1 + + Table Row 1, Table Cell 2 + + Table Row 1, Table Cell 3 + + Table Row 1, Table Cell 4 + + Table Row 1, Table Cell 5 +
+ Table Row 2, Table Cell 1 + + Table Row 2, Table Cell 2 + + Table Row 2, Table Cell 3 + + Table Row 2, Table Cell 4 + + Table Row 2, Table Cell 5 +
+ Table Row 3, Table Cell 1 + + Table Row 3, Table Cell 2 + + Table Row 3, Table Cell 3 + + Table Row 3, Table Cell 4 + + Table Row 3, Table Cell 5 +
+ Table Row 4, Table Cell 1 + + Table Row 4, Table Cell 2 + + Table Row 4, Table Cell 3 + + Table Row 4, Table Cell 4 + + Table Row 4, Table Cell 5 +
+`; + +exports[` FiltersInteractive story renders snapshot 1`] = ` +
+
+ +
@@ -12,29 +188,24 @@ exports[`
AlignTableCellContentCenter story renders snapshot 1`] = ` class="table-row table-row--header" > - @@ -45,28 +216,80 @@ exports[`
- Name - - Status + Food - Chart + Soup - Window + Salad - Last Update + Sandwich
AlignTableCellContentCenter story renders snapshot 1`] = ` + + + AlignTableCellContentCenter story renders snapshot 1`] = ` + + + AlignTableCellContentCenter story renders snapshot 1`] = ` + + + AlignTableCellContentCenter story renders snapshot 1`] = ` + + + + + + + + +
- Table Row 1, Table Cell 1 + Cereal - Table Row 1, Table Cell 2 + + + is soup + + + - Table Row 1, Table Cell 3 + + + is salad + + + +
- Table Row 1, Table Cell 4 + Fried Rice + - Table Row 1, Table Cell 5 + + + is salad + + +
- Table Row 2, Table Cell 1 + Hot Dog + + - Table Row 2, Table Cell 2 + + + is sandwich + + +
- Table Row 2, Table Cell 3 + Mashed Potatoes - Table Row 2, Table Cell 4 + + + is soup + + + - Table Row 2, Table Cell 5 + + + is salad + + +
- Table Row 3, Table Cell 1 + Nigiri + - Table Row 3, Table Cell 2 + + + is salad + + + - Table Row 3, Table Cell 3 + + + is sandwich + + +
- Table Row 3, Table Cell 4 + Oatmeal - Table Row 3, Table Cell 5 + + + is soup + + + +
- Table Row 4, Table Cell 1 + Soup in Bread Bowl - Table Row 4, Table Cell 2 + + + is soup + + + + - Table Row 4, Table Cell 3 + + + is sandwich + + +
- Table Row 4, Table Cell 4 + Sloppy Joe + + + + is soup + + + + + + + is salad + + + + + + + is sandwich + + +
- Table Row 4, Table Cell 5 + Vanilla Soy Latte + + + + is soup + + + +
`; -exports[` AlignTableCellContentRight story renders snapshot 1`] = ` -
SortableInteractive story renders snapshot 1`] = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Not sortable +
+ Value 1 + + Value A +
+ Value 3 + + Value B +
+ Value 2 + + Value C +
+ Value 4 + + Value D +
+`; + +exports[` StackedCardsExample story renders snapshot 1`] = ` +
@@ -172,29 +715,24 @@ exports[`
AlignTableCellContentRight story renders snapshot 1`] = ` class="table-row table-row--header" > - @@ -205,27 +743,22 @@ exports[`
- Name + Students Status - Chart - - Window + Cog skill - Last Update + Time submitted
AlignTableCellContentRight story renders snapshot 1`] = ` - AlignTableCellContentRight story renders snapshot 1`] = ` - AlignTableCellContentRight story renders snapshot 1`] = ` - AlignTableCellContentRight story renders snapshot 1`] = ` - - -
- Table Row 1, Table Cell 1 - - Table Row 1, Table Cell 2 + Araya, Raquel - Table Row 1, Table Cell 3 + Stop and Revise - Table Row 1, Table Cell 4 + 3 - Table Row 1, Table Cell 5 + 2 hours ago
- Table Row 2, Table Cell 1 - - Table Row 2, Table Cell 2 + Jesse Banet - Table Row 2, Table Cell 3 + Stop and Revise - Table Row 2, Table Cell 4 + 1 - Table Row 2, Table Cell 5 + 3 days ago
- Table Row 3, Table Cell 1 - - Table Row 3, Table Cell 2 + Julie Davis - Table Row 3, Table Cell 3 + Working - Table Row 3, Table Cell 4 + 0 - Table Row 3, Table Cell 5 + 3 days ago
- Table Row 4, Table Cell 1 - - Table Row 4, Table Cell 2 + Hussain, Adnan - Table Row 4, Table Cell 3 + Review Feedback - Table Row 4, Table Cell 4 + 0 - Table Row 4, Table Cell 5 + 3 days ago
-
-`; - -exports[` Default story renders snapshot 1`] = ` -
-
- - - - - - - Default story renders snapshot 1`] = ` - Default story renders snapshot 1`] = ` - Default story renders snapshot 1`] = ` - @@ -480,1139 +958,161 @@ exports[`
- Name - - Status - + - Chart - - + - Window - - + - Last Update - + 3 days ago +
- Table Row 1, Table Cell 1 - - Table Row 1, Table Cell 2 + Jaffer, Arman - Table Row 1, Table Cell 3 + Needs Feedback - Table Row 1, Table Cell 4 + 1 - Table Row 1, Table Cell 5 + 3 days ago
- Table Row 2, Table Cell 1 - - Table Row 2, Table Cell 2 + Kang, Michelle - Table Row 2, Table Cell 3 + Review Feedback - Table Row 2, Table Cell 4 + 1 - Table Row 2, Table Cell 5 + 3 days ago
- Table Row 3, Table Cell 1 - - Table Row 3, Table Cell 2 + Lewine, Chris - Table Row 3, Table Cell 3 + Continue Working - Table Row 3, Table Cell 4 + 2 - Table Row 3, Table Cell 5 + 3 days ago
- Table Row 4, Table Cell 1 - - Table Row 4, Table Cell 2 + Luo, Celia - Table Row 4, Table Cell 3 + Review Feedback - Table Row 4, Table Cell 4 + 1 - Table Row 4, Table Cell 5 + 3 days ago
Default story renders snapshot 1`] = ` `; -exports[`
FiltersInteractive story renders snapshot 1`] = ` -
TableWithCaption story renders snapshot 1`] = ` +
-
-
- -
-
+ Optional caption for the table. Must be first descendant of Table if used. + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Food - - Soup - - Salad - - Sandwich -
- Cereal - - - - is soup - - - - - - - is salad - - - - -
- Fried Rice - - - - - is salad - - - - -
- Hot Dog - - - - - - is sandwich - - - -
- Mashed Potatoes - - - - is soup - - - - - - - is salad - - - - -
- Nigiri - - - - - is salad - - - - - - - is sandwich - - - -
- Oatmeal - - - - is soup - - - - - -
- Soup in Bread Bowl - - - - is soup - - - - - - - - is sandwich - - - -
- Sloppy Joe - - - - is soup - - - - - - - is salad - - - - - - - is sandwich - - - -
- Vanilla Soy Latte - - - - is soup - - - - - -
-
-
-`; - -exports[` SortableInteractive story renders snapshot 1`] = ` -
-
- - - - - - - - + - - - + - - - + - - - + - - - -
- - - Not sortable -
- - Value 1 - - Value A -
- - Value 3 - - Value B -
- - Value 2 - - Value C -
- - Value 4 - - Value D -
-
-`; - -exports[` StackedCardsExample story renders snapshot 1`] = ` -
-
-
+ + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Students - - Status - - Cog skill - - Time submitted -
- Araya, Raquel - - Stop and Revise - - 3 - - 2 hours ago -
- Jesse Banet - - Stop and Revise - - 1 - - 3 days ago -
- Julie Davis - - Working - - 0 - - 3 days ago -
- Hussain, Adnan - - Review Feedback - - 0 - - 3 days ago -
- Ilango, Megha - - Needs Feedback - - 3 - - 3 days ago -
- Jaffer, Arman - - Needs Feedback - - 1 - - 3 days ago -
- Kang, Michelle - - Review Feedback - - 1 - - 3 days ago -
- Lewine, Chris - - Continue Working - - 2 - - 3 days ago -
- Luo, Celia - - Review Feedback - - 1 - - 3 days ago -
-
-
-`; - -exports[` TableWithCaption story renders snapshot 1`] = ` -
-
- - - - - - - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - -
- Optional caption for the table. Must be first descendant of Table if used. -
- Name - - Status - - Chart - - Window - - Last Update -
- - Table Row 1, Table Cell 1 - - Table Row 1, Table Cell 2 - - Table Row 1, Table Cell 3 - - Table Row 1, Table Cell 4 - - Table Row 1, Table Cell 5 -
- - Table Row 2, Table Cell 1 - - Table Row 2, Table Cell 2 - - Table Row 2, Table Cell 3 - - Table Row 2, Table Cell 4 - - Table Row 2, Table Cell 5 -
- - Table Row 3, Table Cell 1 - - Table Row 3, Table Cell 2 - - Table Row 3, Table Cell 3 - - Table Row 3, Table Cell 4 - - Table Row 3, Table Cell 5 -
- - Table Row 4, Table Cell 1 - - Table Row 4, Table Cell 2 - - Table Row 4, Table Cell 3 - - Table Row 4, Table Cell 4 - - Table Row 4, Table Cell 5 -
-
-`; - -exports[` ZebraHover story renders snapshot 1`] = ` -
-
- - + + + - - - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - -
- - Name - - Status - - Chart - - Window - - Last Update -
- - Table Row 1, Table Cell 1 - - Table Row 1, Table Cell 2 - - Table Row 1, Table Cell 3 - - Table Row 1, Table Cell 4 - - Table Row 1, Table Cell 5 -
- - Table Row 2, Table Cell 1 - - Table Row 2, Table Cell 2 - - Table Row 2, Table Cell 3 - - Table Row 2, Table Cell 4 - - Table Row 2, Table Cell 5 -
- - Table Row 3, Table Cell 1 - - Table Row 3, Table Cell 2 - - Table Row 3, Table Cell 3 - - Table Row 3, Table Cell 4 - - Table Row 3, Table Cell 5 -
- - Table Row 4, Table Cell 1 - - Table Row 4, Table Cell 2 - - Table Row 4, Table Cell 3 - - Table Row 4, Table Cell 4 - - Table Row 4, Table Cell 5 -
-
+ Table Row 2, Table Cell 5 + + + + + Table Row 3, Table Cell 1 + + + Table Row 3, Table Cell 2 + + + Table Row 3, Table Cell 3 + + + Table Row 3, Table Cell 4 + + + Table Row 3, Table Cell 5 + + + + + Table Row 4, Table Cell 1 + + + Table Row 4, Table Cell 2 + + + Table Row 4, Table Cell 3 + + + Table Row 4, Table Cell 4 + + + Table Row 4, Table Cell 5 + + + + `; diff --git a/src/components/Tag/Tag.module.css b/src/components/Tag/Tag.module.css index 788eef82f..1cc07989b 100644 --- a/src/components/Tag/Tag.module.css +++ b/src/components/Tag/Tag.module.css @@ -16,9 +16,8 @@ display: inline-flex; align-items: center; - /* Grabs colors from whichever variant is selected. */ - border: var(--tag-secondary-color) solid var(--eds-theme-border-width); - /* Rounds the corners of the tag. */ + /* border color matches the background color, unless a color variant is applied */ + border: var(--eds-theme-border-width) solid var(--tag-secondary-color) ; border-radius: var(--eds-border-radius-full); background-color: var(--tag-secondary-color); diff --git a/src/components/Tag/Tag.stories.tsx b/src/components/Tag/Tag.stories.tsx index 4801d0519..1d0155864 100644 --- a/src/components/Tag/Tag.stories.tsx +++ b/src/components/Tag/Tag.stories.tsx @@ -25,10 +25,14 @@ export default { } as Meta; type Args = React.ComponentProps; +type Story = StoryObj; -export const Default: StoryObj = {}; +export const Default: Story = {}; -export const ColorVariants: StoryObj = { +/** + * `Tag` variants correspond to named use cases. Each variant defines a text color, background/surface color, and potential outline color. + */ +export const Variants: Story = { render: (args) => (
{VARIANTS.map((variant) => { @@ -46,7 +50,10 @@ export const ColorVariants: StoryObj = { ), }; -export const OutlineVariants: StoryObj = { +/** + * `Tag` can have an outside border, which corresponds to the variant selected. When false, the border will be set to match the background color. + */ +export const OutlineVariants: Story = { render: (args) => (
{VARIANTS.map((variant) => { @@ -64,7 +71,10 @@ export const OutlineVariants: StoryObj = { ), }; -export const WithIcon: StoryObj = { +/** + * Icons can be added to the left side of the text in the tag. + */ +export const WithIcon: Story = { ...Default, args: { icon: , @@ -86,7 +96,10 @@ export const WithIcon: StoryObj = { ), }; -export const WithLongTextAndIcon: StoryObj = { +/** + * `Tag` can support lengthy text, but should be kept as brief as possible. + */ +export const WithLongTextAndIcon: Story = { ...Default, args: { text: 'This tag has a really long text message', diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx index fa50bb351..1fd7508de 100644 --- a/src/components/Tag/Tag.tsx +++ b/src/components/Tag/Tag.tsx @@ -16,8 +16,9 @@ export type Variant = (typeof VARIANTS)[number]; type Props = { /** - * The color variant of the tag. New variants should be supported - * in the VARIANTS array and by its separate style in CSS file. + * The color variant of the tag. It will update the content colors, background color, and border (when `hasOutline` is set to `true`). + * + * **Default is `"neutral"`**. */ variant?: Variant; /** @@ -25,8 +26,7 @@ type Props = { */ className?: string; /** - * The tag icon (shouldn't provide color or size since those are determined - * by the color prop). + * The tag icon */ icon?: React.ReactNode; /** @@ -34,7 +34,9 @@ type Props = { */ text?: React.ReactNode; /** - * Adds an outline for the tag. Defaulted to no outline. + * Adds an outline for the tag. + * + * **Default is `false`**. */ hasOutline?: boolean; }; @@ -42,7 +44,7 @@ type Props = { /** * `import {Tag} from "@chanzuckerberg/eds";` * - * This component provides a tag (pill shaped badge) wrapper. + * This component provides a tag (pill shaped badge). Can contain text and a left-aligned icon. */ export const Tag = ({ variant = 'neutral', @@ -59,6 +61,8 @@ export const Tag = ({ className, ); + // TODO: Text component is receiving the tag styles directly, instead of using a wrapper. De-couple + // and remove deprecated usages return ( ColorVariants story renders snapshot 1`] = ` +exports[` Default story renders snapshot 1`] = ` + + + Tag text + + +`; + +exports[` OutlineVariants story renders snapshot 1`] = `
ColorVariants story renders snapshot 1`] = ` ColorVariants story renders snapshot 1`] = ` ColorVariants story renders snapshot 1`] = ` ColorVariants story renders snapshot 1`] = ` ColorVariants story renders snapshot 1`] = ` ColorVariants story renders snapshot 1`] = `
`; -exports[` Default story renders snapshot 1`] = ` - - - Tag text - - -`; - -exports[` OutlineVariants story renders snapshot 1`] = ` +exports[` Variants story renders snapshot 1`] = `
OutlineVariants story renders snapshot 1`] = ` OutlineVariants story renders snapshot 1`] = ` OutlineVariants story renders snapshot 1`] = ` OutlineVariants story renders snapshot 1`] = ` OutlineVariants story renders snapshot 1`] = ` = { args: { placeholder: 'Enter long-form text here', defaultValue: `Lorem ipsum, dolor sit amet consectetur adipisicing elit. Id neque nemo - dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo - praesentium, commodi eligendi asperiores quis dolorum porro.`, +dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo +praesentium, commodi eligendi asperiores quis dolorum porro.`, label: 'Textarea Field', rows: 5, fieldNote: 'Longer Field description', @@ -30,7 +30,11 @@ export const Default: Story = { ), }; -export const UsingChildren: Story = { +/** + * You can specify the content of `TextareaField` by using children. Convenient for cases where + * specifying `value` or `defaultValue` is inconvenient. + */ +export const WhenUsingChildren: Story = { args: { children: `Lorem ipsum, dolor sit amet consectetur adipisicing elit. Id neque nemo dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo @@ -39,7 +43,10 @@ export const UsingChildren: Story = { }, }; -export const WithNoDefaultValue: Story = { +/** + * `TextareaField` does not require any initial content. + */ +export const WhenNoDefaultValue: Story = { args: { defaultValue: undefined, fieldNote: undefined, @@ -72,12 +79,21 @@ export const WhenRequired: Story = { }, }; +/** + * You can size `TextareaField` by specifying `row` attribute, inherited from + * [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea). + */ export const WithADifferentSize: Story = { args: { rows: 10, }, }; +/** + * You can lock the maximum length of the text content of `TextareaField`. When setting `maxLength`, + * the field will reuse the browser's [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + * behavior (e.g., prevent further text from being typed, prevent keydown events, etc.). + */ export const WithAMaxLength: Story = { args: { rows: 10, @@ -87,6 +103,11 @@ export const WithAMaxLength: Story = { render: (args) => , }; +/** + * If you want to signal that a field has reached a maximum length but want to allow more text to be typed, you can use + * `recommendedMaxLength`. This will show a similar UI to using `maxLength` but will allow more text to be typed, and + * emit any appropriate events. + */ export const WithARecommendedLength: Story = { args: { rows: 10, @@ -96,6 +117,10 @@ export const WithARecommendedLength: Story = { render: (args) => , }; +/** + * Both `maxLength` and `recommendedMaxLength` can be specified at the same time. Text length between `recommendedMaxLength` + * and `maxLength` will show the treatment warning the user about the text length being violated. + */ export const WithBothRecommendedAndMaxLengths: Story = { args: { rows: 10, diff --git a/src/components/TextareaField/TextareaField.tsx b/src/components/TextareaField/TextareaField.tsx index 6163ee4c2..21d7f9d12 100644 --- a/src/components/TextareaField/TextareaField.tsx +++ b/src/components/TextareaField/TextareaField.tsx @@ -124,7 +124,7 @@ const TextArea = forwardRef( * the content. When a maximum text count is specified, component also shows relevant * text up to the maximum. * - * NOTE: This component requires `label` or `aria-label` prop + * **NOTE**: This component requires `label` or `aria-label` prop to support accessibility. */ export const TextareaField: TextareaFieldType = forwardRef( ( diff --git a/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap b/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap index 8108ddaff..b9f984d47 100644 --- a/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap +++ b/src/components/TextareaField/__snapshots__/TextareaField.test.tsx.snap @@ -24,8 +24,8 @@ exports[` Default story renders snapshot 1`] = ` spellcheck="false" > Lorem ipsum, dolor sit amet consectetur adipisicing elit. Id neque nemo - dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo - praesentium, commodi eligendi asperiores quis dolorum porro. +dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo +praesentium, commodi eligendi asperiores quis dolorum porro. `; -exports[` UsingChildren story renders snapshot 1`] = ` -
-
- -
- - -
-`; - exports[` WhenDisabled story renders snapshot 1`] = `
WhenDisabled story renders snapshot 1`] = ` spellcheck="false" > Lorem ipsum, dolor sit amet consectetur adipisicing elit. Id neque nemo - dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo - praesentium, commodi eligendi asperiores quis dolorum porro. +dicta rerum commodi et fugiat quo optio veniam! Ea odio corporis nemo +praesentium, commodi eligendi asperiores quis dolorum porro.