Skip to content

Commit

Permalink
feat: add support for extended nav and action components (#1918)
Browse files Browse the repository at this point in the history
- add type param.s to Button and Link to handle extending type
- allow use of `as` for extension
- provide code examples in story documentation
  • Loading branch information
booc0mtaco authored Apr 22, 2024
1 parent 665135f commit f4a541e
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 102 deletions.
48 changes: 47 additions & 1 deletion src/components/Button/Button-v2.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -186,3 +186,49 @@ export const IconLayouts: StoryObj<Args> = {
);
},
};

// 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
<Button {...args} onClick={() => 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 `ButtonProps`
* * use the type param. to augment the types for `Button` with the libraries type, e.g., `type ExtendedProps = ButtonProps<typeof CustomButton>;`
* * 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 Button component using `as`
*
* ```tsx
* type ExtendedProps = ButtonProps<typeof CustomButton>;
*
* export default function Button({children, ...other}: ExtendedProps) {
* return (
* <Button as={CustomButton} {...other}>
* {children}
* </Button>
* );
* }
* ```
*/
export const UsingExtendedLink: StoryObj<ExtendArgs> = {
render: (args) => (
<div>
Lorem ipsum dolor sit amet,{' '}
<ExtendedButton {...args} to="test">
consectetur adipiscing elit
</ExtendedButton>
. Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis.
Nullam sit amet iaculis erat. Nulla id tellus ante.{' '}
</div>
),
};
135 changes: 68 additions & 67 deletions src/components/Button/Button-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,73 +9,74 @@ import styles from './Button-v2.module.css';

type ButtonHTMLElementProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

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<Size, 'sm' | 'md' | 'lg'>;

/**
* 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<ExtendedElement = unknown> =
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<Size, 'sm' | 'md' | 'lg'>;

/**
* 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";`
Expand Down
48 changes: 47 additions & 1 deletion src/components/Link/Link-v2.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const Emphasis: StoryObj<Args> = {
},
};

export const LinkInParagraphContext: StoryObj<Args> = {
export const LinkInParagraphContext: StoryObj<ExtendArgs> = {
render: (
args: React.JSX.IntrinsicAttributes &
(LinkProps & React.RefAttributes<HTMLAnchorElement>),
Expand All @@ -86,3 +86,49 @@ export const LinkInParagraphContext: StoryObj<Args> = {
</div>
),
};

// 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
<Link {...args} onClick={() => 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<typeof CustomLink>;`
* * 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<typeof CustomLink>;
*
* export default function Link({children, ...other}: ExtendedProps) {
* return (
* <Link as={CustomLink} {...other}>
* {children}
* </Link>
* );
* }
* ```
*/
export const UsingExtendedLink: StoryObj<ExtendArgs> = {
render: (args) => (
<div>
Lorem ipsum dolor sit amet,{' '}
<ExtendedLink {...args} href="https://go.czi.team/eds" to="test">
consectetur adipiscing elit
</ExtendedLink>
. Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis.
Nullam sit amet iaculis erat. Nulla id tellus ante.{' '}
</div>
),
};
68 changes: 35 additions & 33 deletions src/components/Link/Link-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,42 @@ import Icon from '../Icon';

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

export type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
// 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 `<p>` 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<IconName, 'chevron-right' | 'open-in-new'>;
/**
* Extra or lowered colors added to a link
*/
emphasis?: 'default' | 'high' | 'low';
export type LinkProps<ExtendedElement = unknown> =
React.AnchorHTMLAttributes<HTMLAnchorElement> & {
// 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 `<p>` 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<IconName, 'chevron-right' | 'open-in-new'>;
/**
* Extra or lowered colors added to a link
*/
emphasis?: 'default' | 'high' | 'low';

/**
* Link size inherits from the surrounding text.
*/
size?: Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
};
/**
* Link size inherits from the surrounding text.
*/
size?: Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
// };
} & ExtendedElement;

/**
* `import {Link} from "@chanzuckerberg/eds";`
Expand Down

0 comments on commit f4a541e

Please sign in to comment.