From f4a541e84a8fb91a2a5261eb5542323b30391233 Mon Sep 17 00:00:00 2001 From: Andrew Holloway Date: Mon, 22 Apr 2024 10:46:44 -0500 Subject: [PATCH] feat: add support for extended nav and action components (#1918) - add type param.s to Button and Link to handle extending type - allow use of `as` for extension - provide code examples in story documentation --- src/components/Button/Button-v2.stories.tsx | 48 ++++++- src/components/Button/Button-v2.tsx | 135 ++++++++++---------- src/components/Link/Link-v2.stories.tsx | 48 ++++++- src/components/Link/Link-v2.tsx | 68 +++++----- 4 files changed, 197 insertions(+), 102 deletions(-) diff --git a/src/components/Button/Button-v2.stories.tsx b/src/components/Button/Button-v2.stories.tsx index d239b2e3b..238e510dd 100644 --- a/src/components/Button/Button-v2.stories.tsx +++ b/src/components/Button/Button-v2.stories.tsx @@ -1,6 +1,6 @@ import type { StoryObj, Meta } from '@storybook/react'; import React from 'react'; -import { Button } from './Button-v2'; +import { Button, type ButtonV2Props } from './Button-v2'; import { SIZES } from '../ClickableStyle'; export default { @@ -186,3 +186,49 @@ export const IconLayouts: StoryObj = { ); }, }; + +// Here, we introduce a special type extension to LinkProps, then use it in a +// composed component, to demonstrate the ability to offer custom props to a component +type ExtendArgs = ButtonV2Props<{ to: string }>; +function ExtendedButton(args: ExtendArgs) { + return ( + // eslint-disable-next-line no-alert + + * ); + * } + * ``` + */ +export const UsingExtendedLink: StoryObj = { + render: (args) => ( +
+ Lorem ipsum dolor sit amet,{' '} + + consectetur adipiscing elit + + . Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis. + Nullam sit amet iaculis erat. Nulla id tellus ante.{' '} +
+ ), +}; diff --git a/src/components/Button/Button-v2.tsx b/src/components/Button/Button-v2.tsx index e87111b2e..0c0969155 100644 --- a/src/components/Button/Button-v2.tsx +++ b/src/components/Button/Button-v2.tsx @@ -9,73 +9,74 @@ import styles from './Button-v2.module.css'; type ButtonHTMLElementProps = React.ButtonHTMLAttributes; -type ButtonV2Props = ButtonHTMLElementProps & { - // Component API - /** - * `Button` contents or label. - */ - children: string; - /** - * Determine the behavior of the button upon click: - * - **button** `Button` is a clickable button with no default behavior - * - **submit** `Button` is a clickable button that submits form data - * - **reset** `Button` is a clickable button that resets the form-data to its initial values - */ - type?: 'button' | 'reset' | 'submit'; - - // Design API - /** - * Sets the hierarchy rank of the button - * - * **Default is `"primary"`**. - */ - rank?: 'primary' | 'secondary' | 'tertiary'; - - /** - * The size of the button on screen - */ - size?: Extract; - - /** - * The variant of the default tertiary button. - */ - context?: 'default' | 'standalone'; - - /** - * Icon from the set of defined EDS icon set, when `iconLayout` is used. - */ - icon?: IconName; - - /** - * Allows configuation of the icon's positioning within `Button`. - * - * - When set to a value besides `"none"`, an icon must be specified. - * - When `"icon-only"`, `aria-label` must be given a value. - */ - iconLayout?: 'none' | 'left' | 'right' | 'icon-only'; - - /** - * Status (color) variant for `Button`. - * - * **Default is `"default"`**. - */ - variant?: 'default' | 'critical' | 'inverse'; - - /** - * Whether the width of the button is set to the full layout. - */ - isFullWidth?: boolean; - - /** - * Whether `Button` is set to disabled state (disables interaction and updates appearance). - */ - isDisabled?: boolean; - - /** - * Loading state passed down from higher level used to trigger loader and text change. - */ - isLoading?: boolean; -}; +export type ButtonV2Props = + ButtonHTMLElementProps & { + // Component API + /** + * `Button` contents or label. + */ + children: string; + /** + * Determine the behavior of the button upon click: + * - **button** `Button` is a clickable button with no default behavior + * - **submit** `Button` is a clickable button that submits form data + * - **reset** `Button` is a clickable button that resets the form-data to its initial values + */ + type?: 'button' | 'reset' | 'submit'; + + // Design API + /** + * Sets the hierarchy rank of the button + * + * **Default is `"primary"`**. + */ + rank?: 'primary' | 'secondary' | 'tertiary'; + + /** + * The size of the button on screen + */ + size?: Extract; + + /** + * The variant of the default tertiary button. + */ + context?: 'default' | 'standalone'; + + /** + * Icon from the set of defined EDS icon set, when `iconLayout` is used. + */ + icon?: IconName; + + /** + * Allows configuation of the icon's positioning within `Button`. + * + * - When set to a value besides `"none"`, an icon must be specified. + * - When `"icon-only"`, `aria-label` must be given a value. + */ + iconLayout?: 'none' | 'left' | 'right' | 'icon-only'; + + /** + * Status (color) variant for `Button`. + * + * **Default is `"default"`**. + */ + variant?: 'default' | 'critical' | 'inverse'; + + /** + * Whether the width of the button is set to the full layout. + */ + isFullWidth?: boolean; + + /** + * Whether `Button` is set to disabled state (disables interaction and updates appearance). + */ + isDisabled?: boolean; + + /** + * Loading state passed down from higher level used to trigger loader and text change. + */ + isLoading?: boolean; + } & ExtendedElement; /** * `import {Button} from "@chanzuckerberg/eds";` diff --git a/src/components/Link/Link-v2.stories.tsx b/src/components/Link/Link-v2.stories.tsx index b71b2becd..003e43585 100644 --- a/src/components/Link/Link-v2.stories.tsx +++ b/src/components/Link/Link-v2.stories.tsx @@ -59,7 +59,7 @@ export const Emphasis: StoryObj = { }, }; -export const LinkInParagraphContext: StoryObj = { +export const LinkInParagraphContext: StoryObj = { render: ( args: React.JSX.IntrinsicAttributes & (LinkProps & React.RefAttributes), @@ -86,3 +86,49 @@ export const LinkInParagraphContext: StoryObj = { ), }; + +// Here, we introduce a special type extension to LinkProps, then use it in a +// composed component, to demonstrate the ability to offer custom props to a component +type ExtendArgs = LinkProps<{ to: string }>; +function ExtendedLink(args: ExtendArgs) { + return ( + // eslint-disable-next-line no-alert + alert(`handle to value: ${args.to}`)} /> + ); +} + +/** + * You can extend a component's props for use with libraries that aid navigation, e.g., react-dom-router, et al. + * + * Steps to use: + * + * * import `LinkProps` + * * use the type param. to augment the types for `Link` with the libraries type, e.g., `type ExtendedProps = LinkProps;` + * * Now export a new function component that uses the new prop type and returns a composed function + * + * When using this pattern, you likely want to also specify the library's Link component using `as` + * + * ```tsx + * type ExtendedProps = LinkProps; + * + * export default function Link({children, ...other}: ExtendedProps) { + * return ( + * + * {children} + * + * ); + * } + * ``` + */ +export const UsingExtendedLink: StoryObj = { + render: (args) => ( +
+ Lorem ipsum dolor sit amet,{' '} + + consectetur adipiscing elit + + . Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis. + Nullam sit amet iaculis erat. Nulla id tellus ante.{' '} +
+ ), +}; diff --git a/src/components/Link/Link-v2.tsx b/src/components/Link/Link-v2.tsx index 42a70f249..f1959e0b2 100644 --- a/src/components/Link/Link-v2.tsx +++ b/src/components/Link/Link-v2.tsx @@ -6,40 +6,42 @@ import Icon from '../Icon'; import styles from './Link-v2.module.css'; -export type LinkProps = React.AnchorHTMLAttributes & { - // Component API - /** - * Component used to render the element. Meant to support interaction with framework navigation libraries. - * - * **Default is `"a"`**. - */ - as?: string | React.ElementType; - /** - * The link contents or label. - */ - children: string; - // Design API - /** - * Where `Link` sits alongside other text and content: - * - * * **inline** - Inline link inherits the text size established within the `

` paragraph they are embedded in. - * * **standalone** - Users can choose from the available sizes. - */ - context?: 'inline' | 'standalone'; - /** - * (trailing) icon to use with the link - */ - icon?: Extract; - /** - * Extra or lowered colors added to a link - */ - emphasis?: 'default' | 'high' | 'low'; +export type LinkProps = + React.AnchorHTMLAttributes & { + // Component API + /** + * Component used to render the element. Meant to support interaction with framework navigation libraries. + * + * **Default is `"a"`**. + */ + as?: string | React.ElementType; + /** + * The link contents or label. + */ + children: string; + // Design API + /** + * Where `Link` sits alongside other text and content: + * + * * **inline** - Inline link inherits the text size established within the `

` paragraph they are embedded in. + * * **standalone** - Users can choose from the available sizes. + */ + context?: 'inline' | 'standalone'; + /** + * (trailing) icon to use with the link + */ + icon?: Extract; + /** + * Extra or lowered colors added to a link + */ + emphasis?: 'default' | 'high' | 'low'; - /** - * Link size inherits from the surrounding text. - */ - size?: Extract; -}; + /** + * Link size inherits from the surrounding text. + */ + size?: Extract; + // }; + } & ExtendedElement; /** * `import {Link} from "@chanzuckerberg/eds";`