@kaizen/components 1.72.0 → 1.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,238 @@
1
+ import React from 'react'
2
+ import { type Meta, type StoryObj } from '@storybook/react'
3
+ import { Text } from '~components/Text'
4
+ import { Icon } from '~components/__next__/Icon'
5
+ import { Link } from '../Link'
6
+
7
+ const meta = {
8
+ title: 'Components/Link',
9
+ component: Link,
10
+ args: {
11
+ children: 'Link',
12
+ href: 'https://www.google.com',
13
+ },
14
+ argTypes: {
15
+ variant: {
16
+ options: ['primary', 'secondary', 'white'],
17
+ },
18
+ size: {
19
+ options: ['intro-lede', 'body', 'small', 'extra-small'],
20
+ },
21
+ icon: {
22
+ options: ['arrow_forward', 'open_in_new'],
23
+ mapping: {
24
+ // eslint-disable-next-line camelcase
25
+ arrow_forward: <Icon isPresentational name="arrow_forward" />,
26
+ // eslint-disable-next-line camelcase
27
+ open_in_new: <Icon isPresentational name="open_in_new" />,
28
+ },
29
+ description:
30
+ 'Renders an icon at the specified `iconPosition`. For size scaling, use the `Icon` component from `"@kaizen/components/future"`. See [all available icons](https://cultureamp.design/?path=/docs/components-icon-icon-future-api-specification--docs)',
31
+ },
32
+ },
33
+ } satisfies Meta<typeof Link>
34
+
35
+ export default meta
36
+
37
+ type Story = StoryObj<typeof meta>
38
+
39
+ export const Playground: Story = {
40
+ render: (props) =>
41
+ props.variant !== 'white' ? (
42
+ <Link {...props} />
43
+ ) : (
44
+ <div className="flex p-12 bg-purple-600">
45
+ {' '}
46
+ <Link {...props} />
47
+ </div>
48
+ ),
49
+ }
50
+
51
+ export const LinkVariants: Story = {
52
+ render: (props) => (
53
+ <>
54
+ <Link {...props} variant="primary" />
55
+ <br />
56
+ <Link {...props} variant="secondary" />
57
+ </>
58
+ ),
59
+ decorators: [
60
+ (Story) => (
61
+ <div className="flex gap-8">
62
+ <Story />
63
+ </div>
64
+ ),
65
+ ],
66
+ }
67
+
68
+ export const LinkVariantWhite: Story = {
69
+ render: (props) => <Link {...props} variant="white" />,
70
+ parameters: {
71
+ reverseColors: true,
72
+ },
73
+ }
74
+
75
+ export const LinkWithIconStart: Story = {
76
+ render: (props) => (
77
+ <Link {...props} icon={<Icon name="add" isPresentational />} iconPosition="start" />
78
+ ),
79
+ }
80
+
81
+ export const LinkWithIconEnd: Story = {
82
+ render: (props) => (
83
+ <Link {...props} icon={<Icon name="add" isPresentational />} iconPosition="end" />
84
+ ),
85
+ }
86
+
87
+ export const LinkOpensInNewTab: Story = {
88
+ render: (props) => (
89
+ <Link
90
+ {...props}
91
+ target="_blank"
92
+ icon={<Icon name="open_in_new" isPresentational />}
93
+ iconPosition="end"
94
+ />
95
+ ),
96
+ }
97
+
98
+ export const WithText: Story = {
99
+ render: ({ size: _, ...otherArgs }) => (
100
+ <>
101
+ <Text variant="intro-lede">
102
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae eaque amet atque. Dolores
103
+ repellendus eligendi <span style={{ textDecoration: 'underline' }}> totam.</span>{' '}
104
+ <Link {...otherArgs} icon={<Icon name="add" isPresentational />} isInline /> Mollitia vero
105
+ asperiores assumenda, odit ratione id perspiciatis suscipit molestias quas facere, commodi
106
+ saepe! Quisquam, quidem quas a quos quae quia quidem, quod, voluptates, dolorum quibusdam.
107
+ Quisquam, quidem quas a quos quae
108
+ </Text>
109
+ <br />
110
+ <Text variant="body">
111
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae eaque amet atque. Dolores
112
+ repellendus eligendi <span style={{ textDecoration: 'underline' }}> totam.</span>{' '}
113
+ <Link
114
+ {...otherArgs}
115
+ icon={<Icon name="add" isPresentational />}
116
+ isInline
117
+ size={undefined}
118
+ />
119
+ Mollitia vero asperiores assumenda, odit ratione id perspiciatis suscipit molestias quas
120
+ facere, commodi saepe! Quisquam, quidem quas a quos quae quia quidem, quod, voluptates,
121
+ dolorum quibusdam. Quisquam, quidem quas a quos quae
122
+ </Text>
123
+ <br />
124
+ <Text variant="small">
125
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae eaque amet atque. Dolores
126
+ repellendus eligendi <span style={{ textDecoration: 'underline' }}> totam.</span>{' '}
127
+ <Link {...otherArgs} icon={<Icon name="add" isPresentational />} isInline /> Mollitia vero
128
+ asperiores assumenda, odit ratione id perspiciatis suscipit molestias quas facere, commodi
129
+ saepe! Quisquam, quidem quas a quos quae quia quidem, quod, voluptates, dolorum quibusdam.
130
+ Quisquam, quidem quas a quos quae
131
+ </Text>
132
+ <br />
133
+ <Text variant="extra-small">
134
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae eaque amet atque. Dolores
135
+ repellendus eligendi <span style={{ textDecoration: 'underline' }}> totam.</span>{' '}
136
+ <Link {...otherArgs} icon={<Icon name="add" isPresentational />} isInline /> Mollitia vero
137
+ asperiores assumenda, odit ratione id perspiciatis suscipit molestias quas facere, commodi
138
+ saepe! Quisquam, quidem quas a quos quae quia quidem, quod, voluptates, dolorum quibusdam.
139
+ Quisquam, quidem quas a quos quae
140
+ </Text>
141
+ </>
142
+ ),
143
+ }
144
+
145
+ // Links of every different size
146
+ export const LinkSizes: Story = {
147
+ render: ({ children: _, size: __, isInline: ___, ...otherArgs }) => (
148
+ <>
149
+ <Link size="extra-small" {...otherArgs}>
150
+ Extra Small
151
+ </Link>
152
+ <br />
153
+ <Link {...otherArgs} size="small">
154
+ Small
155
+ </Link>
156
+ <br />
157
+ <Link {...otherArgs} size="body">
158
+ Body
159
+ </Link>
160
+ <br />
161
+ <Link {...otherArgs} size="intro-lede">
162
+ Intro Lede
163
+ </Link>
164
+ </>
165
+ ),
166
+ }
167
+
168
+ export const StandaloneLinkDo: Story = {
169
+ render: (props) => <Link {...props}>Learn more about demographics</Link>,
170
+ }
171
+
172
+ export const StandaloneLinkDont: Story = {
173
+ render: ({ size: _, ...otherArgs }) => (
174
+ <Text variant="body">
175
+ Learn more about{' '}
176
+ <Link {...otherArgs} isInline>
177
+ demographics
178
+ </Link>
179
+ </Text>
180
+ ),
181
+ }
182
+
183
+ export const OneLinkInSentence: Story = {
184
+ render: (props) => (
185
+ <Text variant="body">
186
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Illo ad nobis, ut aspernatur deserunt
187
+ fuga expedita amet architecto{' '}
188
+ <Link {...props} isInline size={undefined}>
189
+ pariatur cum itaque
190
+ </Link>{' '}
191
+ dicta veritatis inventore ea esse rem dolore natus! Architecto.
192
+ </Text>
193
+ ),
194
+ }
195
+
196
+ export const FiveLinksInSentence: Story = {
197
+ render: ({ size: _, ...otherArgs }) => (
198
+ <Text variant="body">
199
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Illo{' '}
200
+ <Link {...otherArgs} isInline>
201
+ {' '}
202
+ ad nobis
203
+ </Link>
204
+ , ut aspernatur{' '}
205
+ <Link {...otherArgs} isInline>
206
+ deserunt fuga expedita amet architecto
207
+ </Link>{' '}
208
+ <Link {...otherArgs} isInline>
209
+ pariatur cum itaque
210
+ </Link>{' '}
211
+ dicta veritatis{' '}
212
+ <Link {...otherArgs} isInline>
213
+ inventore ea
214
+ </Link>{' '}
215
+ esse rem dolore{' '}
216
+ <Link {...otherArgs} isInline>
217
+ natus
218
+ </Link>
219
+ ! Architecto.
220
+ </Text>
221
+ ),
222
+ }
223
+
224
+ export const ExternalIconLink: Story = {
225
+ render: (props) => <Link {...props} icon={<Icon name="open_in_new" isPresentational />} />,
226
+ }
227
+
228
+ export const RandomIconLink: Story = {
229
+ render: (props) => <Link {...props} icon={<Icon name="flag" isPresentational />} />,
230
+ }
231
+
232
+ export const DistinctNamedLink: Story = {
233
+ render: (props) => <Link {...props}>View Q4 2024 dataset</Link>,
234
+ }
235
+
236
+ export const GenericNamedLink: Story = {
237
+ render: (props) => <Link {...props}>Learn more</Link>,
238
+ }
@@ -0,0 +1,191 @@
1
+ import React from 'react'
2
+ import { type Meta } from '@storybook/react'
3
+ import { within } from '@storybook/test'
4
+ import { Icon } from '~components/__next__/Icon'
5
+ import { StickerSheet, type StickerSheetStory } from '~storybook/components/StickerSheet'
6
+ import { Link, type LinkProps } from '../Link'
7
+
8
+ export default {
9
+ title: 'Components/Link',
10
+ component: Link,
11
+ parameters: {
12
+ chromatic: { disable: false },
13
+ controls: { disable: true },
14
+ },
15
+ } satisfies Meta
16
+
17
+ const variants = ['primary', 'secondary'] satisfies LinkProps['variant'][]
18
+ const sizes = ['extra-small', 'small', 'body', 'intro-lede'] satisfies LinkProps['size'][]
19
+ const href = 'https://www.google.com' satisfies LinkProps['href']
20
+
21
+ const StickerSheetTemplate: StickerSheetStory = {
22
+ render: ({ isReversed }) => (
23
+ <>
24
+ <StickerSheet
25
+ title="Link"
26
+ headers={[
27
+ 'isUnderlined',
28
+ 'isUnderlined + Icon Start',
29
+ 'isUnderlined + Icon End',
30
+ 'Icon start',
31
+ 'Icon end',
32
+ 'isDisabled',
33
+ ]}
34
+ isReversed={isReversed}
35
+ >
36
+ {variants.map((variant) =>
37
+ sizes.map((size) => (
38
+ <StickerSheet.Row key={size + variant} header={`${variant} (${size})`}>
39
+ <Link
40
+ variant={isReversed ? 'white' : variant}
41
+ size={size}
42
+ href={href}
43
+ isUnderlined={true}
44
+ isInline={false}
45
+ >
46
+ Link
47
+ </Link>
48
+ <Link
49
+ variant={isReversed ? 'white' : variant}
50
+ size={size}
51
+ href={href}
52
+ isUnderlined={true}
53
+ isInline={false}
54
+ icon={<Icon name="add" isPresentational />}
55
+ iconPosition="start"
56
+ >
57
+ Link
58
+ </Link>
59
+ <Link
60
+ variant={isReversed ? 'white' : variant}
61
+ size={size}
62
+ href={href}
63
+ isUnderlined={true}
64
+ isInline={false}
65
+ icon={<Icon name="add" isPresentational />}
66
+ iconPosition="end"
67
+ >
68
+ Link
69
+ </Link>
70
+ <Link
71
+ variant={isReversed ? 'white' : variant}
72
+ size={size}
73
+ href={href}
74
+ isUnderlined={false}
75
+ isInline={false}
76
+ icon={<Icon name="add" isPresentational />}
77
+ iconPosition="start"
78
+ >
79
+ Link
80
+ </Link>
81
+ <Link
82
+ variant={isReversed ? 'white' : variant}
83
+ size={size}
84
+ href={href}
85
+ isUnderlined={false}
86
+ isInline={false}
87
+ icon={<Icon name="add" isPresentational />}
88
+ iconPosition="end"
89
+ >
90
+ Link
91
+ </Link>
92
+ <Link
93
+ variant={isReversed ? 'white' : variant}
94
+ size={size}
95
+ href={href}
96
+ isUnderlined={true}
97
+ isInline={false}
98
+ isDisabled={true}
99
+ >
100
+ Link
101
+ </Link>
102
+ </StickerSheet.Row>
103
+ )),
104
+ )}
105
+ </StickerSheet>
106
+ <StickerSheet
107
+ title="Pseudo states"
108
+ headers={['isHovered', 'isFocusVisible', 'isPressed']}
109
+ isReversed={isReversed}
110
+ >
111
+ {variants.map((variant) => (
112
+ <StickerSheet.Row key={variant} isReversed={isReversed} header={variant}>
113
+ <Link
114
+ variant={isReversed ? 'white' : variant}
115
+ size="small"
116
+ href={href}
117
+ isUnderlined={true}
118
+ isInline={false}
119
+ icon={<Icon name="add" isPresentational />}
120
+ data-testid="testid__link-hover"
121
+ >
122
+ Label
123
+ </Link>
124
+ <Link
125
+ variant={isReversed ? 'white' : variant}
126
+ size="small"
127
+ href={href}
128
+ isUnderlined={true}
129
+ isInline={false}
130
+ icon={<Icon name="add" isPresentational />}
131
+ data-testid="testid__link-focus"
132
+ >
133
+ Label
134
+ </Link>
135
+ <Link
136
+ variant={isReversed ? 'white' : variant}
137
+ size="small"
138
+ href={href}
139
+ isUnderlined={true}
140
+ isInline={false}
141
+ icon={<Icon name="add" isPresentational />}
142
+ data-testid="testid__link-pressed"
143
+ >
144
+ Label
145
+ </Link>
146
+ </StickerSheet.Row>
147
+ ))}
148
+ </StickerSheet>
149
+ </>
150
+ ),
151
+ play: ({ canvasElement }) => {
152
+ const canvas = within(canvasElement)
153
+ const focusLinks = canvas.getAllByTestId('testid__link-focus')
154
+ const hoverLinks = canvas.getAllByTestId('testid__link-hover')
155
+ const pressedLinks = canvas.getAllByTestId('testid__link-pressed')
156
+
157
+ focusLinks.forEach((Link) => {
158
+ Link.setAttribute('data-focus-visible', 'true')
159
+ })
160
+ hoverLinks.forEach((Link) => {
161
+ Link.setAttribute('data-hovered', 'true')
162
+ })
163
+ pressedLinks.forEach((Link) => {
164
+ Link.setAttribute('data-pressed', 'true')
165
+ })
166
+ },
167
+ }
168
+
169
+ export const StickerSheetDefault: StickerSheetStory = {
170
+ ...StickerSheetTemplate,
171
+ name: 'Sticker Sheet (Default)',
172
+ }
173
+
174
+ export const StickerSheetRTL: StickerSheetStory = {
175
+ ...StickerSheetTemplate,
176
+ name: 'Sticker Sheet (RTL)',
177
+ parameters: {
178
+ textDirection: 'rtl',
179
+ },
180
+ }
181
+
182
+ export const StickerSheetWhite: StickerSheetStory = {
183
+ ...StickerSheetTemplate,
184
+ name: 'Sticker Sheet (White)',
185
+ parameters: {
186
+ reverseColors: true,
187
+ },
188
+ args: {
189
+ isReversed: true,
190
+ },
191
+ }
@@ -0,0 +1 @@
1
+ export * from './Link'
@@ -0,0 +1,31 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import { mergeClassNames } from '~components/utils/mergeClassNames'
3
+ import styles from '../Link.module.css'
4
+
5
+ export type LinkContentProps = {
6
+ children: ReactNode
7
+ icon?: JSX.Element
8
+ iconPosition?: 'start' | 'end'
9
+ isUnderlined: boolean
10
+ }
11
+
12
+ const LinkIcon = ({ icon }: { icon: JSX.Element }): JSX.Element => (
13
+ <span className={styles.icon}>{icon}</span>
14
+ )
15
+
16
+ export const LinkContent = ({
17
+ children,
18
+ icon,
19
+ iconPosition,
20
+ isUnderlined,
21
+ }: LinkContentProps): JSX.Element => {
22
+ const iconPositionStyling = iconPosition === 'start' ? styles.iconStart : styles.iconEnd
23
+
24
+ return (
25
+ <span className={mergeClassNames(styles.linkContent, isUnderlined && styles.isUnderlined)}>
26
+ {icon && iconPosition === 'start' && <LinkIcon icon={icon} />}
27
+ <span className={mergeClassNames(icon && iconPositionStyling)}>{children}</span>
28
+ {icon && iconPosition === 'end' && <LinkIcon icon={icon} />}
29
+ </span>
30
+ )
31
+ }
@@ -163,63 +163,7 @@ For resizing on smaller screens, consider using the `className` prop to leverage
163
163
 
164
164
  ## Client side routing
165
165
 
166
- To enable client side routing with the `LinkButton`, you will need to wrap your application in a [RouterProvider](https://react-spectrum.adobe.com/react-aria/routing.html#routerprovider) from the `react-aria-components` library. The allows you to set the `navigation` method that performs the client side routing in the `LinkButton` component. Refer to the framework specific guidance below for [Next.js](#nextjs-config-example) or [React Router](#react-router-config-example).
167
-
168
- ### Next.js config example
169
-
170
- The following example demonstrates how you might use the React Aria's `RouterProvider` with `Next.js`'s Pages router. This will allow the `LinkButton` to navigate using the `router.push` method.
171
-
172
- ```tsx
173
- // ...imports
174
- import type { AppProps } from 'next/app'
175
- import { type NextRouter } from 'next/router'
176
- import { RouterProvider as RacRouterProvider } from 'react-aria-components'
177
-
178
- // This provides the correct types for `routerOptions` based on the routing solution. As the component agnostic to routing technology this must defined here
179
- declare module 'react-aria-components' {
180
- interface RouterConfig {
181
- // index 2 is the types for the pages routerOptions
182
- routerOptions: NonNullable<Parameters<NextRouter['push']>[2]>
183
- }
184
- }
185
-
186
- function App({ Component, pageProps, router }: AppProps) {
187
- return (
188
- <FrontendServices {...config}>
189
- {/* application code */}
190
- <RacRouterProvider navigate={(href, opts) => router.push(href, undefined, opts)}>
191
- <Component {...pageProps} />
192
- </RacRouterProvider>
193
- {/* application code */}
194
- </FrontendServices>
195
- )
196
- }
197
-
198
- export default App
199
- ```
200
-
201
- The implementation in your application would then look something like this:
202
-
203
- ```tsx
204
- import { useRouter } from 'next/router'
205
- import { LinkButton } from '@kaizen/components'
206
-
207
- const Component = () => {
208
- const router = useRouter()
209
-
210
- return (
211
- <>
212
- <LinkButton href="http://google.com">External link</LinkButton>
213
- <LinkButton href={`${router.pathname}/path-1`}>Internal link</LinkButton>
214
- <LinkButton href={`${router.pathname}/path-2`} routerOptions={{ scroll: false }}>
215
- Link with routerOptions
216
- </LinkButton>
217
- </>
218
- )
219
- }
220
- ```
221
-
222
- Additional config options for Next.js can be found in the React Aria's documentation on the [RouterProvider](https://react-spectrum.adobe.com/react-aria/routing.html#nextjs), including the alternative setup for the [App router](https://react-spectrum.adobe.com/react-aria/routing.html##app-router).
166
+ Please refer to the [client side routing](/docs/guides-client-side-routing--docs) for more information on how to set up client side routing with the `LinkButton`.
223
167
 
224
168
  ### React Router config example
225
169
 
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export * from './Input'
30
30
  export * from './KaizenProvider'
31
31
  export * from './Label'
32
32
  export * from './LabelledMessage'
33
+ export * from './Link'
33
34
  export * from './LikertScaleLegacy'
34
35
  export * from './LinkButton'
35
36
  export * from './Loading'