@kaizen/components 0.0.0-canary-package-bundler-v2-20241113071536 → 0.0.0-canary-link-button-wip-canary-20241121010628
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.
- package/codemods/README.md +16 -1
- package/codemods/upgradeIconV1/getNewIconPropsFromOldIconName.ts +9 -9
- package/dist/cjs/EmptyState/EmptyState.cjs +15 -17
- package/dist/cjs/EmptyState/EmptyState.module.css.cjs +20 -0
- package/dist/cjs/GuidanceBlock/GuidanceBlock.cjs +1 -1
- package/dist/cjs/GuidanceBlock/GuidanceBlock.module.css.cjs +28 -0
- package/dist/cjs/__actions__/Button/v3/Button.cjs +43 -8
- package/dist/cjs/__actions__/Button/v3/Button.module.css.cjs +21 -0
- package/dist/cjs/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.cjs +37 -0
- package/dist/cjs/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.module.css.cjs +9 -0
- package/dist/cjs/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.cjs +35 -0
- package/dist/cjs/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.module.css.cjs +8 -0
- package/dist/cjs/__overlays__/Tooltip/v3/Tooltip.cjs +2 -2
- package/dist/esm/Calendar/CalendarPopover/CalendarPopover.mjs +1 -1
- package/dist/esm/EmptyState/EmptyState.mjs +16 -18
- package/dist/esm/EmptyState/EmptyState.module.css.mjs +18 -0
- package/dist/esm/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.mjs +1 -1
- package/dist/esm/GuidanceBlock/GuidanceBlock.mjs +1 -1
- package/dist/esm/GuidanceBlock/GuidanceBlock.module.css.mjs +26 -0
- package/dist/esm/MultiSelect/subcomponents/Popover/Popover.mjs +1 -1
- package/dist/esm/RichTextEditor/RichTextEditor/RichTextEditor.mjs +1 -1
- package/dist/esm/RichTextEditor/RichTextEditor/schema.mjs +1 -1
- package/dist/esm/RichTextEditor/utils/schema/nodes.mjs +1 -1
- package/dist/esm/TimeField/TimeField.mjs +1 -1
- package/dist/esm/__actions__/Button/v3/Button.mjs +44 -9
- package/dist/esm/__actions__/Button/v3/Button.module.css.mjs +19 -0
- package/dist/esm/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.mjs +28 -0
- package/dist/esm/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.module.css.mjs +7 -0
- package/dist/esm/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.mjs +26 -0
- package/dist/esm/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.module.css.mjs +6 -0
- package/dist/esm/__overlays__/Tooltip/v1/Tooltip.mjs +1 -1
- package/dist/esm/__overlays__/Tooltip/v3/Tooltip.mjs +1 -1
- package/dist/styles.css +547 -463
- package/dist/types/EmptyState/EmptyState.d.ts +2 -1
- package/dist/types/__actions__/Button/v3/Button.d.ts +17 -4
- package/dist/types/__actions__/Button/v3/index.d.ts +1 -0
- package/dist/types/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.d.ts +11 -0
- package/dist/types/__actions__/Button/v3/subcomponents/ButtonContent/index.d.ts +1 -0
- package/dist/types/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.d.ts +5 -0
- package/dist/types/__actions__/Button/v3/subcomponents/PendingContent/index.d.ts +1 -0
- package/dist/types/__actions__/Button/v3/subcomponents/index.d.ts +2 -0
- package/dist/types/__actions__/Button/v3/types.d.ts +21 -0
- package/dist/types/__actions__/LinkButton/index.d.ts +1 -0
- package/dist/types/__actions__/LinkButton/v3/LinkButton.d.ts +11 -0
- package/dist/types/__actions__/LinkButton/v3/index.d.ts +1 -0
- package/package.json +8 -8
- package/src/EmptyState/EmptyState.module.css +114 -0
- package/src/EmptyState/EmptyState.tsx +18 -20
- package/src/EmptyState/_docs/EmptyState.stickersheet.stories.tsx +55 -39
- package/src/GuidanceBlock/{GuidanceBlock.module.scss → GuidanceBlock.module.css} +60 -114
- package/src/GuidanceBlock/GuidanceBlock.tsx +1 -1
- package/src/__actions__/Button/v3/Button.module.css +235 -0
- package/src/__actions__/Button/v3/Button.tsx +95 -29
- package/src/__actions__/Button/v3/_docs/Button--api-specification.mdx +151 -0
- package/src/__actions__/Button/v3/_docs/Button--usage-guidelines.mdx +30 -0
- package/src/__actions__/Button/v3/_docs/Button.docs.stories.tsx +112 -50
- package/src/__actions__/Button/v3/_docs/Button.spec.stories.tsx +80 -120
- package/src/__actions__/Button/v3/_docs/Button.stickersheet.stories.tsx +183 -81
- package/src/__actions__/Button/v3/index.ts +1 -0
- package/src/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.module.css +19 -0
- package/src/__actions__/Button/v3/subcomponents/ButtonContent/ButtonContent.tsx +40 -0
- package/src/__actions__/Button/v3/subcomponents/ButtonContent/index.ts +1 -0
- package/src/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.module.css +16 -0
- package/src/__actions__/Button/v3/subcomponents/PendingContent/PendingContent.tsx +28 -0
- package/src/__actions__/Button/v3/subcomponents/PendingContent/index.ts +1 -0
- package/src/__actions__/Button/v3/subcomponents/index.ts +2 -0
- package/src/__actions__/Button/v3/types.ts +25 -0
- package/src/__actions__/LinkButton/index.ts +1 -0
- package/src/__actions__/LinkButton/v3/LinkButton.module.css +4 -0
- package/src/__actions__/LinkButton/v3/LinkButton.tsx +71 -0
- package/src/__actions__/LinkButton/v3/_docs/LinkButton--api-specification.mdx +200 -0
- package/src/__actions__/LinkButton/v3/_docs/LinkButton.doc.stories.tsx +131 -0
- package/src/__actions__/LinkButton/v3/_docs/LinkButton.spec.stories.tsx +100 -0
- package/src/__actions__/LinkButton/v3/index.ts +1 -0
- package/src/__actions__/Menu/v3/_docs/Menu.docs.stories.tsx +54 -18
- package/src/__actions__/Menu/v3/_docs/Menu.spec.stories.tsx +30 -10
- package/src/__actions__/Menu/v3/_docs/Menu.stories.tsx +12 -4
- package/src/__future__/Icon/_docs/Icon.docs.stories.tsx +7 -7
- package/src/__overlays__/Tooltip/v3/Tooltip.tsx +1 -1
- package/src/__overlays__/Tooltip/v3/_docs/Tooltip.spec.stories.tsx +2 -0
- package/dist/cjs/EmptyState/EmptyState.module.scss.cjs +0 -23
- package/dist/cjs/GuidanceBlock/GuidanceBlock.module.scss.cjs +0 -33
- package/dist/cjs/__actions__/Button/v3/Button.module.scss.cjs +0 -9
- package/dist/esm/EmptyState/EmptyState.module.scss.mjs +0 -21
- package/dist/esm/GuidanceBlock/GuidanceBlock.module.scss.mjs +0 -31
- package/dist/esm/__actions__/Button/v3/Button.module.scss.mjs +0 -7
- package/src/EmptyState/EmptyState.module.scss +0 -177
- package/src/EmptyState/EmptyState.spec.tsx +0 -48
- package/src/EmptyState/_mixins.scss +0 -44
- package/src/__actions__/Button/v3/Button.module.scss +0 -104
- package/src/__actions__/Button/v3/_docs/ApiSpecification.mdx +0 -173
- package/src/__actions__/Button/v3/_docs/Button.mdx +0 -41
- package/src/__actions__/Button/v3/_docs/Button.stories.tsx +0 -98
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ButtonVariants = "primary" | "secondary" | "tertiary"
|
|
2
|
+
|
|
3
|
+
export type ButtonSizes = "small" | "medium" | "large"
|
|
4
|
+
|
|
5
|
+
export type PendingPropsUndefined = {
|
|
6
|
+
isPending?: undefined
|
|
7
|
+
/** Rendered as the child while `isPending` is `true`. This determines the accessible label for the Button while pending. */
|
|
8
|
+
pendingLabel?: never
|
|
9
|
+
/** Visually hides the `pendingLabel` and renders the loading spinner. This will maintain the width of the Button's `children` to avoid layout shifts.
|
|
10
|
+
* @default false
|
|
11
|
+
*/
|
|
12
|
+
hasHiddenPendingLabel?: never
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PendingProps = {
|
|
16
|
+
isPending: boolean
|
|
17
|
+
/** Rendered as the child while `pendingLabel` is `true`. This determines the accessible label for the Button while pending. */
|
|
18
|
+
pendingLabel: string
|
|
19
|
+
/** Visually Hides the `pendingLabel` and renders the loading spinner. This will maintain the width of the Button's `children` to avoid layout shifts.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
hasHiddenPendingLabel?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PendingButtonProps = PendingProps | PendingPropsUndefined
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./v3"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { forwardRef } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Link as RACLink,
|
|
4
|
+
LinkProps as RACLinkProps,
|
|
5
|
+
} from "react-aria-components"
|
|
6
|
+
import { ButtonBaseProps } from "~components/__actions__/Button/v3"
|
|
7
|
+
import buttonStyles from "~components/__actions__/Button/v3/Button.module.css"
|
|
8
|
+
import { ButtonContent } from "~components/__actions__/Button/v3/subcomponents"
|
|
9
|
+
import { useReversedColors } from "~components/__utilities__/v3"
|
|
10
|
+
import { mergeClassNames } from "~components/utils/mergeClassNames"
|
|
11
|
+
import styles from "./LinkButton.module.css"
|
|
12
|
+
|
|
13
|
+
export type LinkButtonProps = ButtonBaseProps &
|
|
14
|
+
Omit<RACLinkProps, "children"> & {
|
|
15
|
+
/** Used as the label for the LinkButton. */
|
|
16
|
+
children: RACLinkProps["children"]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const LinkButton = forwardRef(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
children,
|
|
23
|
+
variant = "primary",
|
|
24
|
+
size = "medium",
|
|
25
|
+
icon,
|
|
26
|
+
iconPosition = "start",
|
|
27
|
+
hasHiddenLabel = false,
|
|
28
|
+
isFullWidth = false,
|
|
29
|
+
isDisabled,
|
|
30
|
+
className,
|
|
31
|
+
...otherProps
|
|
32
|
+
}: LinkButtonProps,
|
|
33
|
+
ref: React.ForwardedRef<HTMLAnchorElement>
|
|
34
|
+
) => {
|
|
35
|
+
const isReversed = useReversedColors()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<RACLink
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={mergeClassNames(
|
|
41
|
+
styles.linkButton,
|
|
42
|
+
buttonStyles.button,
|
|
43
|
+
buttonStyles[size],
|
|
44
|
+
hasHiddenLabel && buttonStyles[`${size}IconButton`],
|
|
45
|
+
isDisabled && buttonStyles.isDisabled,
|
|
46
|
+
isReversed
|
|
47
|
+
? buttonStyles[`${variant}Reversed`]
|
|
48
|
+
: buttonStyles[variant],
|
|
49
|
+
isFullWidth && buttonStyles.fullWidth,
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...otherProps}
|
|
53
|
+
>
|
|
54
|
+
{racStateProps => {
|
|
55
|
+
const childIsFunction = typeof children === "function"
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ButtonContent
|
|
59
|
+
size={size}
|
|
60
|
+
icon={icon}
|
|
61
|
+
iconPosition={iconPosition}
|
|
62
|
+
hasHiddenLabel={hasHiddenLabel}
|
|
63
|
+
>
|
|
64
|
+
{childIsFunction ? children(racStateProps) : children}
|
|
65
|
+
</ButtonContent>
|
|
66
|
+
)
|
|
67
|
+
}}
|
|
68
|
+
</RACLink>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls, ArgTypes, DocsStory } from "@storybook/blocks"
|
|
2
|
+
import { ResourceLinks, KAIOInstallation } from "~storybook/components"
|
|
3
|
+
import { LinkTo } from "../../../../../../../docs/components/LinkTo"
|
|
4
|
+
import * as exampleStories from "./LinkButton.doc.stories"
|
|
5
|
+
|
|
6
|
+
<Meta title="Actions/LinkButton/LinkButton (v3)/API Specification" />
|
|
7
|
+
|
|
8
|
+
# Button API Specification (v3)
|
|
9
|
+
|
|
10
|
+
Updated Nov 12, 2024
|
|
11
|
+
|
|
12
|
+
<ResourceLinks
|
|
13
|
+
sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/__actions__/LinkButton/v3"
|
|
14
|
+
figma="https://www.figma.com/design/eZKEE5kXbEMY3lx84oz8iN/branch/sPhYSlgPScLOAOYfAbkaI5/%F0%9F%92%9C-Heart-UI-Kit?node-id=1929-17364&node-type=canvas&m=dev"
|
|
15
|
+
designGuidelines="/?path=/docs/actions-linkbutton-linkbutton-v3-usage-guidelines--docs"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<KAIOInstallation exportNames={["LinkButton" ]} family="actions" version="3" />
|
|
19
|
+
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
`LinkButton` allows users to navigate to another page or resource within a web page or application.
|
|
23
|
+
|
|
24
|
+
The following example and table showcases the essential props that enable the core functionality of `LinkButton`. For the remaining suite of API options refer to [this section](#additional-api-options).
|
|
25
|
+
|
|
26
|
+
<Canvas of={exampleStories.Playground} />
|
|
27
|
+
|
|
28
|
+
<Controls of={exampleStories.Playground} include={["className", "children", "href", "target", "download", "onPress", "routerOptions", "hasHiddenLabel", "size", "variant", "icon", "iconPosition", "isFullWidth", "isDisabled" ]} />
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
This is built on top of [React Aria's Link component](https://react-spectrum.adobe.com/react-aria/Link.html) and is the counterpart to the <LinkTo pageId="actions-button-button-v3-api-specification--docs">Kaizen Button</LinkTo>, handling icons, variants and sizes in the same way. It provides a semantic wrapper for navigational buttons and allows for native `href` navigation and client side routing with [additional configuration](#client-side-routing) .
|
|
33
|
+
|
|
34
|
+
### Navigation and routing
|
|
35
|
+
|
|
36
|
+
{/* // TODO: basic docs on href usage */}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Client side routing
|
|
40
|
+
|
|
41
|
+
While the `LinkButton` supports native `href` navigation out of the box, it is agnostic to the routing technology chosen. As such, additional set up is required to support client side routing from `React Router` or `Next.js`.
|
|
42
|
+
|
|
43
|
+
In order to achieve this, you can use the [RouterProvider](https://react-spectrum.adobe.com/react-aria/routing.html#routerprovider) from the `react-aria-components` library, along with the additional configuration for each routing solution.
|
|
44
|
+
|
|
45
|
+
#### Next.js config example
|
|
46
|
+
|
|
47
|
+
The following example demonstrates how to use the `RouterProvider` with `Next.js`'s Pages router. This will allow the `LinkButton` to navigate using the `router.push` method.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import type {AppProps} from "next/app"
|
|
51
|
+
import {type NextRouter, useRouter} from "next/router"
|
|
52
|
+
import { RouterProvider } from "react-aria-components"
|
|
53
|
+
|
|
54
|
+
// This provides the necessary typings for the routerOptions to uses of LinkButton across the app
|
|
55
|
+
declare module "react-aria-components" {
|
|
56
|
+
interface RouterConfig {
|
|
57
|
+
routerOptions: NonNullable<Parameters<NextRouter["push"]>[2]>;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function NextPageRouterApp({ Component, pageProps }: AppProps) {
|
|
62
|
+
const router = useRouter()
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<RouterProvider
|
|
66
|
+
navigate={(href, opts) => router.push(href, undefined, opts)}
|
|
67
|
+
>
|
|
68
|
+
{/* ...application code */}
|
|
69
|
+
</RouterProvider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Additional notes on Next.js config can be found in the React Aria's documentation on the [RouterProvider](https://react-spectrum.adobe.com/react-aria/routing.html#routerprovider), including the alternative setup for the [App router](https://react-spectrum.adobe.com/react-aria/routing.html##app-router).
|
|
75
|
+
|
|
76
|
+
#### React Router config example
|
|
77
|
+
The following example demonstrates how to use the `RouterProvider` with `React Router`'s. This will allow the `LinkButton` to navigate using the `useNavigate` hook.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { RouterProvider } from "react-aria-components"
|
|
81
|
+
import { BrowserRouter, NavigateOptions, useHref, useNavigate } from "react-router-dom"
|
|
82
|
+
|
|
83
|
+
declare module "react-aria-components" {
|
|
84
|
+
interface RouterConfig {
|
|
85
|
+
routerOptions: NavigateOptions
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ReactRouterApp() {
|
|
90
|
+
const navigate = useNavigate()
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<RouterProvider navigate={navigate} useHref={useHref}>
|
|
94
|
+
{/* ...application code */}
|
|
95
|
+
<Routes>
|
|
96
|
+
<Route path="/" element={<HomePage />} />
|
|
97
|
+
{/* ...routes */}
|
|
98
|
+
</Routes>
|
|
99
|
+
</RouterProvider>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Additional notes can be found in the React Aria's documentation on the using the `RouterProvider` with [React Router](https://react-spectrum.adobe.com/react-aria/routing.html#react-router).
|
|
105
|
+
|
|
106
|
+
### Variants
|
|
107
|
+
|
|
108
|
+
`LinkButton` supports the following variants: `primary`, `secondary` and `tertiary`. If the `variant` prop is not specified, the default will be `primary`.
|
|
109
|
+
|
|
110
|
+
<Canvas of={exampleStories.LinkButtonVariants} />
|
|
111
|
+
|
|
112
|
+
Reversed variants are handled via the `ReversedColors` Provider.
|
|
113
|
+
|
|
114
|
+
<DocsStory of={exampleStories.LinkButtonVariantsReversed} expanded={false} />
|
|
115
|
+
|
|
116
|
+
To enable the reversed theme, you will need to wrap the component or application in the `ReversedColors` provider, ie:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { RouterProvider } from "react-aria-components"
|
|
120
|
+
import { Button } from "@kaizen/components/v3/actions"
|
|
121
|
+
import { ReversedColors } from "@kaizen/components/v3/utilities"
|
|
122
|
+
// application code
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ReversedColors isReversed={true}>
|
|
126
|
+
<LinkButton {...LinkbuttonProps} />
|
|
127
|
+
</ReversedColors>
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Sizes
|
|
132
|
+
|
|
133
|
+
LinkButton supports the following sizes: `small`, `medium` and `large`. If the `size` prop is not specified, the default will be `medium`.
|
|
134
|
+
|
|
135
|
+
<Canvas of={exampleStories.LinkButtonSizes} />
|
|
136
|
+
|
|
137
|
+
### `onPress`
|
|
138
|
+
|
|
139
|
+
TODO: revamp this section: One key change to the `LinkButton`'s API is the shift from `onClick` to React Aria's implementation of `onPress`. This approach has been adopted as it provides better support for consistent touch events across device types, such mobile, desktop and tablet. You can read more about the development and reason behind this pattern [here](https://react-spectrum.adobe.com/blog/building-a-button-part-1.html#touch-interactions).
|
|
140
|
+
|
|
141
|
+
Functionally this does not change the way we pass actions into `LinkButton`. Consumers can safely replace `onClick` with `onPress` without any additional changes, ie:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<LinkButton label="Submit" onClick={e => sumbit(e)}/>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Can safely be replaced with the following:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<LinkButton onPress={e => submit(e)}>
|
|
151
|
+
Submit
|
|
152
|
+
</LinkButton>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
### Button content and children
|
|
157
|
+
|
|
158
|
+
Labels and any button content can be passed into the `LinkButton` as `children`. Content wrapped by the `LinkButton` will be spaced evenly relative to the `size` prop.
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
<LinkButton variant="secondary" href="#link">
|
|
162
|
+
Label
|
|
163
|
+
</LinkButton>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
While in most cases, `children` will be a `ReactNode`, `LinkButton` also accepts a render function with React Aria's `LinkRenderProps`. This allows for more advanced styling and rendering options by hooking into React Aria's internal button state. You can read more about this [here](https://react-spectrum.adobe.com/react-aria/Link.html#styling).
|
|
167
|
+
|
|
168
|
+
### Icons and positioning
|
|
169
|
+
|
|
170
|
+
The `icon` property abstracts the need to handle positioning and sizing logic for icons within the `LinkButton`. When paired with the [Icon component](/docs/illustrations-icon-icon-future-api-specification--docs), this will scale the icon to the `LinkButton`'s `size` prop.
|
|
171
|
+
|
|
172
|
+
<Canvas of={exampleStories.LinkButtonWithIconStart} />
|
|
173
|
+
|
|
174
|
+
Set the position of the icon using the `iconPosition` prop. This will ensure content is flipped in `RTL` layouts. Note that icons will need the [shouldMirrorInRTL](/docs/illustrations-icon-icon-future-api-specification--docs#mirror-in-rtl) prop set to `true` when mirroring is required.
|
|
175
|
+
|
|
176
|
+
<Canvas of={exampleStories.LinkButtonWithIconEnd} />
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
### Icon-only `LinkButton` and `hasHiddenLabel`
|
|
180
|
+
|
|
181
|
+
To achieve an icon-only `LinkButton` (previously: `IconButton`) use the `icon` prop and set `hasHiddenLabel` to `true`. This will visually hide the button's `children`, while still announcing the content to screen readers.
|
|
182
|
+
|
|
183
|
+
<Canvas of={exampleStories.IconLinkButton} />
|
|
184
|
+
|
|
185
|
+
This pattern ensures that the `LinkButton`'s accessible name is determined by its children, which helps to announce relevant content to the screen readers. You can learn more about this [accessible pattern here](https://cultureamp.atlassian.net/wiki/spaces/PA/pages/3833331831/Accessible+button+and+link+labels).
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
### Full width LinkButtons
|
|
189
|
+
|
|
190
|
+
If a `LinkButton` is statically the full width of a container you can use the `isFullWidth` property.
|
|
191
|
+
|
|
192
|
+
<Canvas of={exampleStories.LinkButtonFullWidth} />
|
|
193
|
+
|
|
194
|
+
For resizing on smaller screens, consider using the `className` prop to leverage CSS media or container queries, ie: `<LinkButton className="w-full md:w-[initial]">Label</LinkButton>`.
|
|
195
|
+
|
|
196
|
+
## Additional API options
|
|
197
|
+
|
|
198
|
+
The following table is a collection of additional React Aria and native HTML props that are exposed from the [React Aria Link API](https://react-spectrum.adobe.com/react-aria/Link.html). These are not required for the implementation of `LinkButton` but can be used to extend its functionality. Refer back to the [overview section](#overview) for the core props that enable most use cases.
|
|
199
|
+
|
|
200
|
+
<ArgTypes of={exampleStories.Playground} exclude={["className", "children", "href", "target", "download", "routerOptions", "hasHiddenLabel", "size", "variant", "onPress", "icon", "iconPosition", "isFullWidth", "isDisabled" ]}/>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
// import { action } from "@storybook/addon-actions"
|
|
3
|
+
import { Meta, StoryObj } from "@storybook/react"
|
|
4
|
+
import { Badge } from "~components/Badge"
|
|
5
|
+
import { Icon } from "~components/__future__"
|
|
6
|
+
import { ReversedColors } from "~components/__utilities__/v3"
|
|
7
|
+
import { LinkButton } from "../index"
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Actions/LinkButton/LinkButton (v3)",
|
|
11
|
+
component: LinkButton,
|
|
12
|
+
args: {
|
|
13
|
+
children: "Label",
|
|
14
|
+
href: "#link-button-clicked",
|
|
15
|
+
target: "_blank",
|
|
16
|
+
},
|
|
17
|
+
} satisfies Meta<typeof LinkButton>
|
|
18
|
+
|
|
19
|
+
export default meta
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof meta>
|
|
22
|
+
|
|
23
|
+
export const Playground: Story = {}
|
|
24
|
+
|
|
25
|
+
export const LinkButtonVariants: Story = {
|
|
26
|
+
render: args => (
|
|
27
|
+
<>
|
|
28
|
+
<LinkButton {...args} variant="primary" />
|
|
29
|
+
<LinkButton {...args} variant="secondary" />
|
|
30
|
+
<LinkButton {...args} variant="tertiary" />
|
|
31
|
+
</>
|
|
32
|
+
),
|
|
33
|
+
decorators: [
|
|
34
|
+
Story => (
|
|
35
|
+
<div className="flex gap-8">
|
|
36
|
+
<Story />
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const LinkButtonVariantsReversed: Story = {
|
|
43
|
+
render: args => (
|
|
44
|
+
<ReversedColors isReversed={true}>
|
|
45
|
+
<LinkButton {...args} variant="primary" />
|
|
46
|
+
<LinkButton {...args} variant="secondary" />
|
|
47
|
+
<LinkButton {...args} variant="tertiary" />
|
|
48
|
+
</ReversedColors>
|
|
49
|
+
),
|
|
50
|
+
parameters: {
|
|
51
|
+
reverseColors: true,
|
|
52
|
+
},
|
|
53
|
+
decorators: [
|
|
54
|
+
Story => (
|
|
55
|
+
<div className="flex gap-8 bg-purple-700 p-16">
|
|
56
|
+
<Story />
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const LinkButtonSizes: Story = {
|
|
63
|
+
render: args => (
|
|
64
|
+
<>
|
|
65
|
+
<LinkButton {...args} size="small" />
|
|
66
|
+
<LinkButton {...args} size="medium" />
|
|
67
|
+
<LinkButton {...args} size="large" />
|
|
68
|
+
</>
|
|
69
|
+
),
|
|
70
|
+
decorators: [
|
|
71
|
+
Story => (
|
|
72
|
+
<div className="[&>*]:ms-8">
|
|
73
|
+
<Story />
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const LinkButtonWithIconStart: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
icon: <Icon isPresentational name="delete" />,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const LinkButtonWithIconEnd: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
icon: <Icon isPresentational name="arrow_forward" shouldMirrorInRTL />,
|
|
88
|
+
iconPosition: "end",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const IconLinkButton: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
children: "Remove highlights from: May 8, 2024",
|
|
95
|
+
icon: <Icon isPresentational name="delete" />,
|
|
96
|
+
hasHiddenLabel: true,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const ReversedLinkButton: Story = {
|
|
101
|
+
parameters: {
|
|
102
|
+
reverseColors: true,
|
|
103
|
+
docs: {
|
|
104
|
+
source: {
|
|
105
|
+
code: `<ReversedColors isReversed={true}>
|
|
106
|
+
<LinkButton>Label</LinkButton>
|
|
107
|
+
</ReversedColors>
|
|
108
|
+
`,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const LinkButtonFullWidth: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
isFullWidth: true,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const LinkButtonWithBadge: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
children: (
|
|
123
|
+
<>
|
|
124
|
+
Label
|
|
125
|
+
<Badge classNameOverride="ms-8" size="small">
|
|
126
|
+
3
|
|
127
|
+
</Badge>
|
|
128
|
+
</>
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { userEvent, waitFor, within, expect } from "@storybook/test"
|
|
4
|
+
import { VisuallyHidden } from "~components/VisuallyHidden"
|
|
5
|
+
import { Icon } from "~components/__future__/Icon"
|
|
6
|
+
import { LinkButton } from "../index"
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Actions/LinkButton/LinkButton (v3)/LinkButton (v3) tests",
|
|
10
|
+
component: LinkButton,
|
|
11
|
+
args: {
|
|
12
|
+
children: "Label",
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof LinkButton>
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>
|
|
19
|
+
|
|
20
|
+
export const IconLinkButtonWithHiddenLabel: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
hasHiddenLabel: true,
|
|
23
|
+
icon: <Icon name="add" isPresentational />,
|
|
24
|
+
children: (
|
|
25
|
+
<span>
|
|
26
|
+
Hidden label <span>is</span> <span>accessible</span>
|
|
27
|
+
</span>
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
play: async ({ canvasElement }) => {
|
|
31
|
+
const canvas = within(canvasElement.parentElement!)
|
|
32
|
+
const linkButton = canvas.getByRole("link")
|
|
33
|
+
|
|
34
|
+
expect(linkButton).toHaveAccessibleName("Hidden label is accessible")
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const RACRenderPropsWithChildren: Story = {
|
|
39
|
+
render: ({ children: _, ...otherArgs }) => (
|
|
40
|
+
<LinkButton {...otherArgs}>
|
|
41
|
+
{({ isFocusVisible }) => (
|
|
42
|
+
<>
|
|
43
|
+
Label
|
|
44
|
+
<VisuallyHidden>
|
|
45
|
+
{isFocusVisible ? " is focused" : " is unfocused"}
|
|
46
|
+
</VisuallyHidden>
|
|
47
|
+
<Icon
|
|
48
|
+
name={isFocusVisible ? "thumb_up" : "thumb_down"}
|
|
49
|
+
isPresentational
|
|
50
|
+
isFilled={true}
|
|
51
|
+
className="ms-8 [--icon-size:16]"
|
|
52
|
+
/>
|
|
53
|
+
</>
|
|
54
|
+
)}
|
|
55
|
+
</LinkButton>
|
|
56
|
+
),
|
|
57
|
+
play: async ({ canvasElement, step }) => {
|
|
58
|
+
const canvas = within(canvasElement.parentElement!)
|
|
59
|
+
const linkButton = canvas.getByRole("link")
|
|
60
|
+
|
|
61
|
+
await step("link icon reflects unfocused state", async () => {
|
|
62
|
+
await waitFor(() =>
|
|
63
|
+
expect(linkButton).toHaveAccessibleName("Label is unfocused")
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await step("focus on link and update icon", async () => {
|
|
68
|
+
await userEvent.tab()
|
|
69
|
+
await waitFor(() =>
|
|
70
|
+
expect(linkButton).toHaveAccessibleName("Label is focused")
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const RACRenderPropsWithClassName: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
className: ({ isFocusVisible }) => (isFocusVisible ? "!bg-gray-300" : ""),
|
|
79
|
+
},
|
|
80
|
+
play: async ({ canvasElement }) => {
|
|
81
|
+
const canvas = within(canvasElement.parentElement!)
|
|
82
|
+
const linkButton = canvas.getByRole("link")
|
|
83
|
+
await linkButton.focus()
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Test cases:
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Can route to a new page when clicked
|
|
91
|
+
*
|
|
92
|
+
* has accepts routing options from a client side router (will have to mock this)
|
|
93
|
+
*
|
|
94
|
+
* can have href and onPress at the same item
|
|
95
|
+
*
|
|
96
|
+
* can have href, onPress and download
|
|
97
|
+
*
|
|
98
|
+
* ping will send a post request with body ping to URL
|
|
99
|
+
*
|
|
100
|
+
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LinkButton"
|
|
@@ -41,8 +41,12 @@ const DefaultMenuItems = (): ReactNode => (
|
|
|
41
41
|
export const Actions: Story = {
|
|
42
42
|
render: ({ defaultOpen: _, ...args }) => (
|
|
43
43
|
<MenuTrigger {...args}>
|
|
44
|
-
<Button
|
|
45
|
-
|
|
44
|
+
<Button
|
|
45
|
+
size="large"
|
|
46
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
47
|
+
hasHiddenLabel
|
|
48
|
+
>
|
|
49
|
+
Additional actions
|
|
46
50
|
</Button>
|
|
47
51
|
<Popover>
|
|
48
52
|
<Menu>
|
|
@@ -59,8 +63,12 @@ export const Actions: Story = {
|
|
|
59
63
|
export const ItemsDo: Story = {
|
|
60
64
|
render: ({ defaultOpen, ...args }) => (
|
|
61
65
|
<MenuTrigger defaultOpen={defaultOpen} {...args}>
|
|
62
|
-
<Button
|
|
63
|
-
|
|
66
|
+
<Button
|
|
67
|
+
size="large"
|
|
68
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
69
|
+
hasHiddenLabel
|
|
70
|
+
>
|
|
71
|
+
Additional actions
|
|
64
72
|
</Button>
|
|
65
73
|
<Popover>
|
|
66
74
|
<Menu>
|
|
@@ -74,8 +82,12 @@ export const ItemsDo: Story = {
|
|
|
74
82
|
export const ItemsDont: Story = {
|
|
75
83
|
render: ({ defaultOpen, ...args }) => (
|
|
76
84
|
<MenuTrigger defaultOpen={defaultOpen} {...args}>
|
|
77
|
-
<Button
|
|
78
|
-
|
|
85
|
+
<Button
|
|
86
|
+
size="large"
|
|
87
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
88
|
+
hasHiddenLabel
|
|
89
|
+
>
|
|
90
|
+
Additional actions
|
|
79
91
|
</Button>
|
|
80
92
|
<Popover>
|
|
81
93
|
<Menu>
|
|
@@ -171,8 +183,12 @@ export const LabelDont: Story = {
|
|
|
171
183
|
export const IconsDont: Story = {
|
|
172
184
|
render: ({ defaultOpen, ...args }) => (
|
|
173
185
|
<MenuTrigger defaultOpen={defaultOpen}>
|
|
174
|
-
<Button
|
|
175
|
-
|
|
186
|
+
<Button
|
|
187
|
+
size="large"
|
|
188
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
189
|
+
hasHiddenLabel
|
|
190
|
+
>
|
|
191
|
+
Additional actions
|
|
176
192
|
</Button>
|
|
177
193
|
<Popover>
|
|
178
194
|
<Menu {...args}>
|
|
@@ -193,8 +209,12 @@ export const IconsDont: Story = {
|
|
|
193
209
|
export const MenuItemLabelsDont: Story = {
|
|
194
210
|
render: ({ defaultOpen, ...args }) => (
|
|
195
211
|
<MenuTrigger defaultOpen={defaultOpen}>
|
|
196
|
-
<Button
|
|
197
|
-
|
|
212
|
+
<Button
|
|
213
|
+
size="large"
|
|
214
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
215
|
+
hasHiddenLabel
|
|
216
|
+
>
|
|
217
|
+
Additional actions
|
|
198
218
|
</Button>
|
|
199
219
|
<Popover>
|
|
200
220
|
<Menu {...args}>
|
|
@@ -210,8 +230,12 @@ export const MenuItemLabelsDont: Story = {
|
|
|
210
230
|
export const SentenceCaseDo: Story = {
|
|
211
231
|
render: ({ defaultOpen, ...args }) => (
|
|
212
232
|
<MenuTrigger defaultOpen={defaultOpen}>
|
|
213
|
-
<Button
|
|
214
|
-
|
|
233
|
+
<Button
|
|
234
|
+
size="large"
|
|
235
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
236
|
+
hasHiddenLabel
|
|
237
|
+
>
|
|
238
|
+
Additional actions
|
|
215
239
|
</Button>
|
|
216
240
|
<Popover>
|
|
217
241
|
<Menu {...args}>
|
|
@@ -227,8 +251,12 @@ export const SentenceCaseDo: Story = {
|
|
|
227
251
|
export const SentenceCaseDont: Story = {
|
|
228
252
|
render: ({ defaultOpen, ...args }) => (
|
|
229
253
|
<MenuTrigger defaultOpen={defaultOpen}>
|
|
230
|
-
<Button
|
|
231
|
-
|
|
254
|
+
<Button
|
|
255
|
+
size="large"
|
|
256
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
257
|
+
hasHiddenLabel
|
|
258
|
+
>
|
|
259
|
+
Additional actions
|
|
232
260
|
</Button>
|
|
233
261
|
<Popover>
|
|
234
262
|
<Menu {...args}>
|
|
@@ -244,8 +272,12 @@ export const SentenceCaseDont: Story = {
|
|
|
244
272
|
export const ElipsesDo: Story = {
|
|
245
273
|
render: ({ defaultOpen, ...args }) => (
|
|
246
274
|
<MenuTrigger defaultOpen={defaultOpen}>
|
|
247
|
-
<Button
|
|
248
|
-
|
|
275
|
+
<Button
|
|
276
|
+
size="large"
|
|
277
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
278
|
+
hasHiddenLabel
|
|
279
|
+
>
|
|
280
|
+
Additional actions
|
|
249
281
|
</Button>
|
|
250
282
|
<Popover>
|
|
251
283
|
<Menu {...args}>
|
|
@@ -261,8 +293,12 @@ export const ElipsesDo: Story = {
|
|
|
261
293
|
export const ElipsesDont: Story = {
|
|
262
294
|
render: ({ defaultOpen, ...args }) => (
|
|
263
295
|
<MenuTrigger defaultOpen={defaultOpen}>
|
|
264
|
-
<Button
|
|
265
|
-
|
|
296
|
+
<Button
|
|
297
|
+
size="large"
|
|
298
|
+
icon={<Icon name="more_horiz" isPresentational />}
|
|
299
|
+
hasHiddenLabel
|
|
300
|
+
>
|
|
301
|
+
Additional actions
|
|
266
302
|
</Button>
|
|
267
303
|
<Popover>
|
|
268
304
|
<Menu {...args}>
|