Skip to content

Commit f4a541e

Browse files
authored
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
1 parent 665135f commit f4a541e

File tree

4 files changed

+197
-102
lines changed

4 files changed

+197
-102
lines changed

src/components/Button/Button-v2.stories.tsx

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { StoryObj, Meta } from '@storybook/react';
22
import React from 'react';
3-
import { Button } from './Button-v2';
3+
import { Button, type ButtonV2Props } from './Button-v2';
44
import { SIZES } from '../ClickableStyle';
55

66
export default {
@@ -186,3 +186,49 @@ export const IconLayouts: StoryObj<Args> = {
186186
);
187187
},
188188
};
189+
190+
// Here, we introduce a special type extension to LinkProps, then use it in a
191+
// composed component, to demonstrate the ability to offer custom props to a component
192+
type ExtendArgs = ButtonV2Props<{ to: string }>;
193+
function ExtendedButton(args: ExtendArgs) {
194+
return (
195+
// eslint-disable-next-line no-alert
196+
<Button {...args} onClick={() => alert(`handle to value: ${args.to}`)} />
197+
);
198+
}
199+
200+
/**
201+
* You can extend a component's props for use with libraries that aid navigation, e.g., react-dom-router, et al.
202+
*
203+
* Steps to use:
204+
*
205+
* * import `ButtonProps`
206+
* * use the type param. to augment the types for `Button` with the libraries type, e.g., `type ExtendedProps = ButtonProps<typeof CustomButton>;`
207+
* * Now export a new function component that uses the new prop type and returns a composed function
208+
*
209+
* When using this pattern, you likely want to also specify the library's Button component using `as`
210+
*
211+
* ```tsx
212+
* type ExtendedProps = ButtonProps<typeof CustomButton>;
213+
*
214+
* export default function Button({children, ...other}: ExtendedProps) {
215+
* return (
216+
* <Button as={CustomButton} {...other}>
217+
* {children}
218+
* </Button>
219+
* );
220+
* }
221+
* ```
222+
*/
223+
export const UsingExtendedLink: StoryObj<ExtendArgs> = {
224+
render: (args) => (
225+
<div>
226+
Lorem ipsum dolor sit amet,{' '}
227+
<ExtendedButton {...args} to="test">
228+
consectetur adipiscing elit
229+
</ExtendedButton>
230+
. Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis.
231+
Nullam sit amet iaculis erat. Nulla id tellus ante.{' '}
232+
</div>
233+
),
234+
};

src/components/Button/Button-v2.tsx

+68-67
Original file line numberDiff line numberDiff line change
@@ -9,73 +9,74 @@ import styles from './Button-v2.module.css';
99

1010
type ButtonHTMLElementProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
1111

12-
type ButtonV2Props = ButtonHTMLElementProps & {
13-
// Component API
14-
/**
15-
* `Button` contents or label.
16-
*/
17-
children: string;
18-
/**
19-
* Determine the behavior of the button upon click:
20-
* - **button** `Button` is a clickable button with no default behavior
21-
* - **submit** `Button` is a clickable button that submits form data
22-
* - **reset** `Button` is a clickable button that resets the form-data to its initial values
23-
*/
24-
type?: 'button' | 'reset' | 'submit';
25-
26-
// Design API
27-
/**
28-
* Sets the hierarchy rank of the button
29-
*
30-
* **Default is `"primary"`**.
31-
*/
32-
rank?: 'primary' | 'secondary' | 'tertiary';
33-
34-
/**
35-
* The size of the button on screen
36-
*/
37-
size?: Extract<Size, 'sm' | 'md' | 'lg'>;
38-
39-
/**
40-
* The variant of the default tertiary button.
41-
*/
42-
context?: 'default' | 'standalone';
43-
44-
/**
45-
* Icon from the set of defined EDS icon set, when `iconLayout` is used.
46-
*/
47-
icon?: IconName;
48-
49-
/**
50-
* Allows configuation of the icon's positioning within `Button`.
51-
*
52-
* - When set to a value besides `"none"`, an icon must be specified.
53-
* - When `"icon-only"`, `aria-label` must be given a value.
54-
*/
55-
iconLayout?: 'none' | 'left' | 'right' | 'icon-only';
56-
57-
/**
58-
* Status (color) variant for `Button`.
59-
*
60-
* **Default is `"default"`**.
61-
*/
62-
variant?: 'default' | 'critical' | 'inverse';
63-
64-
/**
65-
* Whether the width of the button is set to the full layout.
66-
*/
67-
isFullWidth?: boolean;
68-
69-
/**
70-
* Whether `Button` is set to disabled state (disables interaction and updates appearance).
71-
*/
72-
isDisabled?: boolean;
73-
74-
/**
75-
* Loading state passed down from higher level used to trigger loader and text change.
76-
*/
77-
isLoading?: boolean;
78-
};
12+
export type ButtonV2Props<ExtendedElement = unknown> =
13+
ButtonHTMLElementProps & {
14+
// Component API
15+
/**
16+
* `Button` contents or label.
17+
*/
18+
children: string;
19+
/**
20+
* Determine the behavior of the button upon click:
21+
* - **button** `Button` is a clickable button with no default behavior
22+
* - **submit** `Button` is a clickable button that submits form data
23+
* - **reset** `Button` is a clickable button that resets the form-data to its initial values
24+
*/
25+
type?: 'button' | 'reset' | 'submit';
26+
27+
// Design API
28+
/**
29+
* Sets the hierarchy rank of the button
30+
*
31+
* **Default is `"primary"`**.
32+
*/
33+
rank?: 'primary' | 'secondary' | 'tertiary';
34+
35+
/**
36+
* The size of the button on screen
37+
*/
38+
size?: Extract<Size, 'sm' | 'md' | 'lg'>;
39+
40+
/**
41+
* The variant of the default tertiary button.
42+
*/
43+
context?: 'default' | 'standalone';
44+
45+
/**
46+
* Icon from the set of defined EDS icon set, when `iconLayout` is used.
47+
*/
48+
icon?: IconName;
49+
50+
/**
51+
* Allows configuation of the icon's positioning within `Button`.
52+
*
53+
* - When set to a value besides `"none"`, an icon must be specified.
54+
* - When `"icon-only"`, `aria-label` must be given a value.
55+
*/
56+
iconLayout?: 'none' | 'left' | 'right' | 'icon-only';
57+
58+
/**
59+
* Status (color) variant for `Button`.
60+
*
61+
* **Default is `"default"`**.
62+
*/
63+
variant?: 'default' | 'critical' | 'inverse';
64+
65+
/**
66+
* Whether the width of the button is set to the full layout.
67+
*/
68+
isFullWidth?: boolean;
69+
70+
/**
71+
* Whether `Button` is set to disabled state (disables interaction and updates appearance).
72+
*/
73+
isDisabled?: boolean;
74+
75+
/**
76+
* Loading state passed down from higher level used to trigger loader and text change.
77+
*/
78+
isLoading?: boolean;
79+
} & ExtendedElement;
7980

8081
/**
8182
* `import {Button} from "@chanzuckerberg/eds";`

src/components/Link/Link-v2.stories.tsx

+47-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const Emphasis: StoryObj<Args> = {
5959
},
6060
};
6161

62-
export const LinkInParagraphContext: StoryObj<Args> = {
62+
export const LinkInParagraphContext: StoryObj<ExtendArgs> = {
6363
render: (
6464
args: React.JSX.IntrinsicAttributes &
6565
(LinkProps & React.RefAttributes<HTMLAnchorElement>),
@@ -86,3 +86,49 @@ export const LinkInParagraphContext: StoryObj<Args> = {
8686
</div>
8787
),
8888
};
89+
90+
// Here, we introduce a special type extension to LinkProps, then use it in a
91+
// composed component, to demonstrate the ability to offer custom props to a component
92+
type ExtendArgs = LinkProps<{ to: string }>;
93+
function ExtendedLink(args: ExtendArgs) {
94+
return (
95+
// eslint-disable-next-line no-alert
96+
<Link {...args} onClick={() => alert(`handle to value: ${args.to}`)} />
97+
);
98+
}
99+
100+
/**
101+
* You can extend a component's props for use with libraries that aid navigation, e.g., react-dom-router, et al.
102+
*
103+
* Steps to use:
104+
*
105+
* * import `LinkProps`
106+
* * use the type param. to augment the types for `Link` with the libraries type, e.g., `type ExtendedProps = LinkProps<typeof CustomLink>;`
107+
* * Now export a new function component that uses the new prop type and returns a composed function
108+
*
109+
* When using this pattern, you likely want to also specify the library's Link component using `as`
110+
*
111+
* ```tsx
112+
* type ExtendedProps = LinkProps<typeof CustomLink>;
113+
*
114+
* export default function Link({children, ...other}: ExtendedProps) {
115+
* return (
116+
* <Link as={CustomLink} {...other}>
117+
* {children}
118+
* </Link>
119+
* );
120+
* }
121+
* ```
122+
*/
123+
export const UsingExtendedLink: StoryObj<ExtendArgs> = {
124+
render: (args) => (
125+
<div>
126+
Lorem ipsum dolor sit amet,{' '}
127+
<ExtendedLink {...args} href="https://go.czi.team/eds" to="test">
128+
consectetur adipiscing elit
129+
</ExtendedLink>
130+
. Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis.
131+
Nullam sit amet iaculis erat. Nulla id tellus ante.{' '}
132+
</div>
133+
),
134+
};

src/components/Link/Link-v2.tsx

+35-33
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,42 @@ import Icon from '../Icon';
66

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

9-
export type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
10-
// Component API
11-
/**
12-
* Component used to render the element. Meant to support interaction with framework navigation libraries.
13-
*
14-
* **Default is `"a"`**.
15-
*/
16-
as?: string | React.ElementType;
17-
/**
18-
* The link contents or label.
19-
*/
20-
children: string;
21-
// Design API
22-
/**
23-
* Where `Link` sits alongside other text and content:
24-
*
25-
* * **inline** - Inline link inherits the text size established within the `<p>` paragraph they are embedded in.
26-
* * **standalone** - Users can choose from the available sizes.
27-
*/
28-
context?: 'inline' | 'standalone';
29-
/**
30-
* (trailing) icon to use with the link
31-
*/
32-
icon?: Extract<IconName, 'chevron-right' | 'open-in-new'>;
33-
/**
34-
* Extra or lowered colors added to a link
35-
*/
36-
emphasis?: 'default' | 'high' | 'low';
9+
export type LinkProps<ExtendedElement = unknown> =
10+
React.AnchorHTMLAttributes<HTMLAnchorElement> & {
11+
// Component API
12+
/**
13+
* Component used to render the element. Meant to support interaction with framework navigation libraries.
14+
*
15+
* **Default is `"a"`**.
16+
*/
17+
as?: string | React.ElementType;
18+
/**
19+
* The link contents or label.
20+
*/
21+
children: string;
22+
// Design API
23+
/**
24+
* Where `Link` sits alongside other text and content:
25+
*
26+
* * **inline** - Inline link inherits the text size established within the `<p>` paragraph they are embedded in.
27+
* * **standalone** - Users can choose from the available sizes.
28+
*/
29+
context?: 'inline' | 'standalone';
30+
/**
31+
* (trailing) icon to use with the link
32+
*/
33+
icon?: Extract<IconName, 'chevron-right' | 'open-in-new'>;
34+
/**
35+
* Extra or lowered colors added to a link
36+
*/
37+
emphasis?: 'default' | 'high' | 'low';
3738

38-
/**
39-
* Link size inherits from the surrounding text.
40-
*/
41-
size?: Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
42-
};
39+
/**
40+
* Link size inherits from the surrounding text.
41+
*/
42+
size?: Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
43+
// };
44+
} & ExtendedElement;
4345

4446
/**
4547
* `import {Link} from "@chanzuckerberg/eds";`

0 commit comments

Comments
 (0)