@kaizen/components 1.64.14 → 1.66.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.
- package/dist/cjs/Loading/LoadingSpinner/LoadingSpinner.cjs +6 -42
- package/dist/cjs/Loading/LoadingSpinner/LoadingSpinner.module.css.cjs +10 -0
- package/dist/cjs/Loading/LoadingSpinner/subcomponents/SpinnerIcon.cjs +80 -0
- package/dist/cjs/__future__/Tabs/Tabs.cjs +23 -0
- package/dist/cjs/__future__/Tabs/subcomponents/Tab/Tab.cjs +39 -0
- package/dist/cjs/__future__/Tabs/subcomponents/Tab/Tab.module.css.cjs +7 -0
- package/dist/cjs/__future__/Tabs/subcomponents/TabList/TabList.cjs +31 -0
- package/dist/cjs/__future__/Tabs/subcomponents/TabList/TabList.module.css.cjs +7 -0
- package/dist/cjs/__future__/Tabs/subcomponents/TabPanel/TabPanel.cjs +24 -0
- package/dist/cjs/future.cjs +8 -0
- package/dist/esm/Loading/LoadingSpinner/LoadingSpinner.mjs +6 -42
- package/dist/esm/Loading/LoadingSpinner/LoadingSpinner.module.css.mjs +8 -0
- package/dist/esm/Loading/LoadingSpinner/subcomponents/SpinnerIcon.mjs +72 -0
- package/dist/esm/__future__/Tabs/Tabs.mjs +15 -0
- package/dist/esm/__future__/Tabs/subcomponents/Tab/Tab.mjs +30 -0
- package/dist/esm/__future__/Tabs/subcomponents/Tab/Tab.module.css.mjs +5 -0
- package/dist/esm/__future__/Tabs/subcomponents/TabList/TabList.mjs +22 -0
- package/dist/esm/__future__/Tabs/subcomponents/TabList/TabList.module.css.mjs +5 -0
- package/dist/esm/__future__/Tabs/subcomponents/TabPanel/TabPanel.mjs +16 -0
- package/dist/esm/future.mjs +4 -0
- package/dist/styles.css +246 -110
- package/dist/types/Loading/LoadingSpinner/LoadingSpinner.d.ts +2 -2
- package/dist/types/Loading/LoadingSpinner/subcomponents/SpinnerIcon.d.ts +5 -0
- package/dist/types/Loading/LoadingSpinner/subcomponents/index.d.ts +1 -0
- package/dist/types/Tabs/subcomponents/index.d.ts +0 -1
- package/dist/types/__future__/Tabs/Tabs.d.ts +11 -0
- package/dist/types/__future__/Tabs/index.d.ts +2 -0
- package/dist/types/__future__/Tabs/subcomponents/Tab/Tab.d.ts +12 -0
- package/dist/types/__future__/Tabs/subcomponents/Tab/index.d.ts +1 -0
- package/dist/types/__future__/Tabs/subcomponents/TabList/TabList.d.ts +17 -0
- package/dist/types/__future__/Tabs/subcomponents/TabList/index.d.ts +1 -0
- package/dist/types/__future__/Tabs/subcomponents/TabPanel/TabPanel.d.ts +6 -0
- package/dist/types/__future__/Tabs/subcomponents/TabPanel/index.d.ts +1 -0
- package/dist/types/__future__/Tabs/subcomponents/index.d.ts +3 -0
- package/dist/types/__future__/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/Loading/LoadingSpinner/LoadingSpinner.module.css +32 -0
- package/src/Loading/LoadingSpinner/LoadingSpinner.tsx +10 -54
- package/src/Loading/LoadingSpinner/_docs/LoadingSpinner.mdx +7 -2
- package/src/Loading/LoadingSpinner/_docs/LoadingSpinner.stickersheet.stories.tsx +2 -1
- package/src/Loading/LoadingSpinner/_docs/LoadingSpinner.stories.tsx +19 -0
- package/src/Loading/LoadingSpinner/subcomponents/SpinnerIcon.tsx +87 -0
- package/src/Loading/LoadingSpinner/subcomponents/index.ts +1 -0
- package/src/Tabs/subcomponents/index.ts +0 -1
- package/src/__actions__/Button/v3/_docs/Button.stickersheet.stories.tsx +2 -2
- package/src/__future__/Tabs/Tabs.tsx +18 -0
- package/src/__future__/Tabs/_docs/Tabs--api-specification.mdx +43 -0
- package/src/__future__/Tabs/_docs/Tabs--migration-guide.mdx +93 -0
- package/src/__future__/Tabs/_docs/Tabs.stories.tsx +74 -0
- package/src/__future__/Tabs/index.ts +2 -0
- package/src/__future__/Tabs/subcomponents/Tab/Tab.module.css +94 -0
- package/src/__future__/Tabs/subcomponents/Tab/Tab.tsx +58 -0
- package/src/__future__/Tabs/subcomponents/Tab/index.ts +1 -0
- package/src/__future__/Tabs/subcomponents/TabList/TabList.module.css +8 -0
- package/src/__future__/Tabs/subcomponents/TabList/TabList.tsx +45 -0
- package/src/__future__/Tabs/subcomponents/TabList/index.ts +1 -0
- package/src/__future__/Tabs/subcomponents/TabPanel/TabPanel.module.css +12 -0
- package/src/__future__/Tabs/subcomponents/TabPanel/TabPanel.tsx +20 -0
- package/src/__future__/Tabs/subcomponents/TabPanel/index.ts +1 -0
- package/src/__future__/Tabs/subcomponents/index.ts +3 -0
- package/src/__future__/index.ts +1 -0
- package/dist/cjs/Loading/LoadingSpinner/LoadingSpinner.module.scss.cjs +0 -7
- package/dist/esm/Loading/LoadingSpinner/LoadingSpinner.module.scss.mjs +0 -5
- package/src/Loading/LoadingSpinner/LoadingSpinner.module.scss +0 -16
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.loadingSpinner {
|
|
2
|
+
--loading-spinner-size: 48px;
|
|
3
|
+
|
|
4
|
+
display: flex;
|
|
5
|
+
width: var(--loading-spinner-size);
|
|
6
|
+
height: var(--loading-spinner-size);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.xs {
|
|
10
|
+
--loading-spinner-size: 16px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.sm {
|
|
14
|
+
--loading-spinner-size: 24px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.md {
|
|
18
|
+
--loading-spinner-size: 48px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.spinner {
|
|
22
|
+
animation: spinner var(--animation-duration-deliberate)
|
|
23
|
+
var(--animation-easing-function-ease-in-out) infinite;
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes spinner {
|
|
29
|
+
100% {
|
|
30
|
+
transform: rotate(360deg);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -2,14 +2,15 @@ import React, { HTMLAttributes } from "react"
|
|
|
2
2
|
import classnames from "classnames"
|
|
3
3
|
import { VisuallyHidden } from "~components/VisuallyHidden"
|
|
4
4
|
import { OverrideClassName } from "~components/types/OverrideClassName"
|
|
5
|
-
import
|
|
5
|
+
import { SpinnerIcon } from "./subcomponents"
|
|
6
|
+
import styles from "./LoadingSpinner.module.css"
|
|
6
7
|
|
|
7
8
|
export type LoadingSpinnerProps = {
|
|
8
9
|
accessibilityLabel: string
|
|
9
10
|
/**
|
|
10
|
-
* Generally use "md" unless spinner is inside a form field
|
|
11
|
+
* Generally use "md" unless spinner is inside a form field. @default "md"
|
|
11
12
|
*/
|
|
12
|
-
size?: "sm" | "md"
|
|
13
|
+
size?: "xs" | "sm" | "md"
|
|
13
14
|
} & OverrideClassName<Omit<HTMLAttributes<HTMLDivElement>, "children">>
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -23,61 +24,16 @@ export const LoadingSpinner = ({
|
|
|
23
24
|
...props
|
|
24
25
|
}: LoadingSpinnerProps): JSX.Element => (
|
|
25
26
|
<div
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
className={classnames(
|
|
28
|
+
styles.loadingSpinner,
|
|
29
|
+
styles[size],
|
|
30
|
+
classNameOverride
|
|
31
|
+
)}
|
|
28
32
|
role="status"
|
|
29
33
|
{...props}
|
|
30
34
|
>
|
|
31
35
|
<VisuallyHidden>{accessibilityLabel}</VisuallyHidden>
|
|
32
|
-
{size
|
|
33
|
-
<svg
|
|
34
|
-
className={styles.spinner}
|
|
35
|
-
aria-hidden="true"
|
|
36
|
-
viewBox="0 0 48 48"
|
|
37
|
-
width={48} // Ideally we'd use spacing tokens converted to unitless values
|
|
38
|
-
fill="none"
|
|
39
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
40
|
-
>
|
|
41
|
-
<circle
|
|
42
|
-
cx="24"
|
|
43
|
-
cy="24"
|
|
44
|
-
r="22.5"
|
|
45
|
-
stroke="currentColor"
|
|
46
|
-
strokeWidth="3"
|
|
47
|
-
strokeOpacity="0.3"
|
|
48
|
-
/>
|
|
49
|
-
<path
|
|
50
|
-
fillRule="evenodd"
|
|
51
|
-
clipRule="evenodd"
|
|
52
|
-
fill="currentColor"
|
|
53
|
-
d="M46.5 24c.8284 0 1.5049-.6734 1.4539-1.5002C47.21 10.44 37.5601.789989 25.5003.0461639 24.6734-.004835 24 .671607 24 1.50003c0 .82843.6738 1.49444 1.5002 1.55277 10.4023.73424 18.7128 9.0447 19.447 19.447C45.0056 23.3262 45.6716 24 46.5 24z"
|
|
54
|
-
/>
|
|
55
|
-
</svg>
|
|
56
|
-
) : (
|
|
57
|
-
<svg
|
|
58
|
-
className={styles.spinner}
|
|
59
|
-
aria-hidden="true"
|
|
60
|
-
viewBox="0 0 24 24"
|
|
61
|
-
width={24} // Ideally we'd use spacing tokens converted to unitless values
|
|
62
|
-
fill="none"
|
|
63
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
64
|
-
>
|
|
65
|
-
<circle
|
|
66
|
-
cx="12"
|
|
67
|
-
cy="12"
|
|
68
|
-
r="9"
|
|
69
|
-
stroke="currentColor"
|
|
70
|
-
strokeWidth="2"
|
|
71
|
-
strokeOpacity="0.3"
|
|
72
|
-
/>
|
|
73
|
-
<path
|
|
74
|
-
fillRule="evenodd"
|
|
75
|
-
clipRule="evenodd"
|
|
76
|
-
fill="currentColor"
|
|
77
|
-
d="M21.0564 13c.5076 0 .9377-.3851.9431-.8926.0004-.0358.0005-.0716.0005-.1074 0-5.52285-4.4771-10-10-10-.0359 0-.0718.00019-.1076.00057-.5076.00535-.8926.43552-.8926.94308v.11543C10.9998 3.59163 11.4675 4 12 4c4.4183 0 8 3.58172 8 8 0 .5324.4083 1 .9407 1h.1157z"
|
|
78
|
-
/>
|
|
79
|
-
</svg>
|
|
80
|
-
)}
|
|
36
|
+
<SpinnerIcon size={size} />
|
|
81
37
|
</div>
|
|
82
38
|
)
|
|
83
39
|
|
|
@@ -31,12 +31,17 @@ When inside a button, it is intended to have the same color as the label text.
|
|
|
31
31
|
|
|
32
32
|
### ClassNameOverride
|
|
33
33
|
|
|
34
|
-
Override styles such as the colour of the spinner using `classNameOverride`.
|
|
34
|
+
Override styles, such as the colour of the spinner using `classNameOverride`.
|
|
35
35
|
|
|
36
36
|
<Canvas of={LoadingSpinnerStories.ClassNameOverride} />
|
|
37
37
|
|
|
38
|
+
Below is an example of how to apply the current color tokens used in designs.
|
|
39
|
+
|
|
40
|
+
<Canvas of={LoadingSpinnerStories.Colors} />
|
|
41
|
+
|
|
38
42
|
### Size
|
|
39
43
|
|
|
40
|
-
Generally use `"md"` (default value) unless spinner is inside a form field.
|
|
44
|
+
Generally use `"md"` (default value) unless spinner is inside a form field, in which case use `sm`. The `xs` size is for buttons or content dense layouts.
|
|
41
45
|
|
|
42
46
|
<Canvas of={LoadingSpinnerStories.Size} />
|
|
47
|
+
|
|
@@ -18,10 +18,11 @@ const StickerSheetTemplate: StickerSheetStory = {
|
|
|
18
18
|
render: ({ isReversed }) => (
|
|
19
19
|
<StickerSheet isReversed={isReversed}>
|
|
20
20
|
<StickerSheet.Header
|
|
21
|
-
headings={['Size "sm"', 'Size "md"', "Custom
|
|
21
|
+
headings={['Size "xs"', 'Size "sm"', 'Size "md"', "Custom color"]}
|
|
22
22
|
/>
|
|
23
23
|
<StickerSheet.Body>
|
|
24
24
|
<StickerSheet.Row>
|
|
25
|
+
<LoadingSpinner accessibilityLabel="Loading" size="xs" />
|
|
25
26
|
<LoadingSpinner accessibilityLabel="Loading" size="sm" />
|
|
26
27
|
<LoadingSpinner accessibilityLabel="Loading" size="md" />
|
|
27
28
|
<LoadingSpinner
|
|
@@ -28,9 +28,28 @@ export const ClassNameOverride: Story = {
|
|
|
28
28
|
args: { classNameOverride: "text-green-400" },
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export const Colors: Story = {
|
|
32
|
+
render: args => (
|
|
33
|
+
<>
|
|
34
|
+
<LoadingSpinner {...args} classNameOverride="text-green-400" />
|
|
35
|
+
<LoadingSpinner {...args} classNameOverride="text-purple-800" />
|
|
36
|
+
<LoadingSpinner {...args} classNameOverride="text-blue-500" />
|
|
37
|
+
<LoadingSpinner {...args} classNameOverride="text-red-500" />
|
|
38
|
+
</>
|
|
39
|
+
),
|
|
40
|
+
decorators: [
|
|
41
|
+
Story => (
|
|
42
|
+
<div className="flex gap-24">
|
|
43
|
+
<Story />
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
export const Size: Story = {
|
|
32
50
|
render: args => (
|
|
33
51
|
<>
|
|
52
|
+
<LoadingSpinner {...args} size="xs" />
|
|
34
53
|
<LoadingSpinner {...args} size="sm" />
|
|
35
54
|
<LoadingSpinner {...args} size="md" />
|
|
36
55
|
</>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import styles from "../LoadingSpinner.module.css"
|
|
3
|
+
|
|
4
|
+
const SmallSpinnerIcon = (): JSX.Element => (
|
|
5
|
+
<svg
|
|
6
|
+
className={styles.spinner}
|
|
7
|
+
aria-hidden="true"
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
>
|
|
12
|
+
<circle
|
|
13
|
+
cx="12"
|
|
14
|
+
cy="12"
|
|
15
|
+
r="9"
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
strokeWidth="2"
|
|
18
|
+
strokeOpacity="0.3"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
fillRule="evenodd"
|
|
22
|
+
clipRule="evenodd"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
d="M21.0564 13c.5076 0 .9377-.3851.9431-.8926.0004-.0358.0005-.0716.0005-.1074 0-5.52285-4.4771-10-10-10-.0359 0-.0718.00019-.1076.00057-.5076.00535-.8926.43552-.8926.94308v.11543C10.9998 3.59163 11.4675 4 12 4c4.4183 0 8 3.58172 8 8 0 .5324.4083 1 .9407 1h.1157z"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const MediumSpinnerIcon = (): JSX.Element => (
|
|
30
|
+
<svg
|
|
31
|
+
className={styles.spinner}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
viewBox="0 0 48 48"
|
|
34
|
+
fill="none"
|
|
35
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
36
|
+
>
|
|
37
|
+
<circle
|
|
38
|
+
cx="24"
|
|
39
|
+
cy="24"
|
|
40
|
+
r="22.5"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
strokeWidth="3"
|
|
43
|
+
strokeOpacity="0.3"
|
|
44
|
+
/>
|
|
45
|
+
<path
|
|
46
|
+
fillRule="evenodd"
|
|
47
|
+
clipRule="evenodd"
|
|
48
|
+
fill="currentColor"
|
|
49
|
+
d="M46.5 24c.8284 0 1.5049-.6734 1.4539-1.5002C47.21 10.44 37.5601.789989 25.5003.0461639 24.6734-.004835 24 .671607 24 1.50003c0 .82843.6738 1.49444 1.5002 1.55277 10.4023.73424 18.7128 9.0447 19.447 19.447C45.0056 23.3262 45.6716 24 46.5 24z"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const ExtraSmallSpinnerIcon = (): JSX.Element => (
|
|
55
|
+
<svg
|
|
56
|
+
className={styles.spinner}
|
|
57
|
+
viewBox="0 0 16 16"
|
|
58
|
+
fill="none"
|
|
59
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
60
|
+
>
|
|
61
|
+
<circle
|
|
62
|
+
opacity="0.3"
|
|
63
|
+
cx="8"
|
|
64
|
+
cy="8"
|
|
65
|
+
r="6"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth="2"
|
|
68
|
+
/>
|
|
69
|
+
<path
|
|
70
|
+
d="M14 8C14 4.68629 11.3137 2 8 2"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
strokeWidth="2"
|
|
73
|
+
strokeLinecap="round"
|
|
74
|
+
strokeLinejoin="round"
|
|
75
|
+
/>
|
|
76
|
+
</svg>
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
type SpinnerIconProps = {
|
|
80
|
+
size: "xs" | "sm" | "md"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const SpinnerIcon = ({ size }: SpinnerIconProps): JSX.Element => {
|
|
84
|
+
if (size === "xs") return <ExtraSmallSpinnerIcon />
|
|
85
|
+
if (size === "sm") return <SmallSpinnerIcon />
|
|
86
|
+
return <MediumSpinnerIcon />
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./SpinnerIcon"
|
|
@@ -82,7 +82,7 @@ const StickerSheetTemplate: StickerSheetStory = {
|
|
|
82
82
|
<ArrowForwardIcon role="presentation" />
|
|
83
83
|
</Button>
|
|
84
84
|
<Button size="small" isDisabled>
|
|
85
|
-
<LoadingSpinner size="
|
|
85
|
+
<LoadingSpinner size="xs" accessibilityLabel="submitting label" />
|
|
86
86
|
</Button>
|
|
87
87
|
</StickerSheet.Row>
|
|
88
88
|
<StickerSheet.Row rowTitle="Icon only small">
|
|
@@ -93,7 +93,7 @@ const StickerSheetTemplate: StickerSheetStory = {
|
|
|
93
93
|
<TrashIcon role="img" aria-label="Remove label" />
|
|
94
94
|
</Button>
|
|
95
95
|
<Button size="small" isDisabled>
|
|
96
|
-
<LoadingSpinner size="
|
|
96
|
+
<LoadingSpinner size="xs" accessibilityLabel="Removing label" />
|
|
97
97
|
</Button>
|
|
98
98
|
</StickerSheet.Row>
|
|
99
99
|
</StickerSheet.Body>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import {
|
|
3
|
+
Tabs as RACTabs,
|
|
4
|
+
TabsProps as RACTabsProps,
|
|
5
|
+
Key as RACKey,
|
|
6
|
+
} from "react-aria-components"
|
|
7
|
+
|
|
8
|
+
export type TabsProps = Omit<RACTabsProps, "orientation">
|
|
9
|
+
export type Key = RACKey
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3081929117/Tabs Guidance} |
|
|
13
|
+
* {@link https://cultureamp.design/?path=/docs/components-tabs--controlled Storybook}
|
|
14
|
+
*
|
|
15
|
+
* Wrapper around all of the tab subcomponents
|
|
16
|
+
* Holds a TabList and TabPanels
|
|
17
|
+
*/
|
|
18
|
+
export const Tabs = (props: TabsProps): JSX.Element => <RACTabs {...props} />
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/blocks"
|
|
2
|
+
import { ResourceLinks, KAIOInstallation } from "~storybook/components"
|
|
3
|
+
import * as TabsStories from "./Tabs.stories"
|
|
4
|
+
|
|
5
|
+
<Meta title="Components/Tabs/Tabs (Future)/API Specification" />
|
|
6
|
+
|
|
7
|
+
# Tabs
|
|
8
|
+
|
|
9
|
+
<ResourceLinks
|
|
10
|
+
sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/Tabs"
|
|
11
|
+
figma="https://www.figma.com/file/eZKEE5kXbEMY3lx84oz8iN/%F0%9F%92%9C-UI-Kit%3A-Heart?type=design&node-id=1929%3A28886&mode=design&t=AGMmnoJia9RscurE-1"
|
|
12
|
+
designGuidelines="https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3081929117/Tabs"
|
|
13
|
+
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<KAIOInstallation
|
|
17
|
+
exportNames={["Tabs", "TabList", "Tab", "TabPanel"]}
|
|
18
|
+
isFuture
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
<Canvas of={TabsStories.Playground} />
|
|
24
|
+
<Controls of={TabsStories.Playground} />
|
|
25
|
+
|
|
26
|
+
## Uncontrolled vs controlled
|
|
27
|
+
|
|
28
|
+
This component is uncontrolled by default. You can specify a default active tab on load with `defaultSelectedKey`.
|
|
29
|
+
|
|
30
|
+
If you need to control the state of the active tabs on the consuming side, use the `selectedKey` prop instead of `defaultSelectedKey`, and hook into `onSelectionChange`.
|
|
31
|
+
|
|
32
|
+
<Canvas of={TabsStories.Controlled} />
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## `tablist|tab` role vs `nav|link`
|
|
36
|
+
|
|
37
|
+
This component implements the `tablist` role and WAI ARIA guidelines for tabs:
|
|
38
|
+
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role
|
|
39
|
+
https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
40
|
+
|
|
41
|
+
It's not intended to be used for a navigation bar where you need links wrapped in a `<nav>` instead.
|
|
42
|
+
|
|
43
|
+
If you really need to, you can add a URL history change here (using onChange), but that's probably a sign that this component is being misused.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Meta } from "@storybook/blocks"
|
|
2
|
+
|
|
3
|
+
<Meta title="Components/Tabs/Tabs (Future)/Migration Guide" />
|
|
4
|
+
|
|
5
|
+
# Future Tabs migration guide
|
|
6
|
+
|
|
7
|
+
A brief guide on how and why to migrate from Kaizen's current `Tabs` to the `future` release.
|
|
8
|
+
|
|
9
|
+
## Why the change?
|
|
10
|
+
|
|
11
|
+
Current Tabs uses the Reach UI library under the hood, which is no longer actively maintained. This switches the library used internally to React Aria Components.
|
|
12
|
+
|
|
13
|
+
## Component and API changes at a glance
|
|
14
|
+
|
|
15
|
+
The Reach UI and React Aria APIs are fairly similar so there's not too much to adjust.
|
|
16
|
+
|
|
17
|
+
The biggest adjustment is that you now need to provide an `id` for each `<Tab>` and match it with the one on `<TabPanel>`
|
|
18
|
+
|
|
19
|
+
Additionally:
|
|
20
|
+
- `<TabPanel>`s no longer needs to be wrapped in a `<TabPanels>` component
|
|
21
|
+
- `classNameOverride` changes to `className`
|
|
22
|
+
- `<Tabs defaultIndex={}>` changes to `<Tabs defaultSelectedKey={}>`
|
|
23
|
+
- `<Tabs index={}>` changes to `<Tabs selectedKey={}>`
|
|
24
|
+
- `<Tabs onChange={}>` changes to `<Tabs onSelectionChange={}>`
|
|
25
|
+
- `<Tab disabled>` changes to `<Tab isDisabled>`
|
|
26
|
+
|
|
27
|
+
## Migration examples
|
|
28
|
+
|
|
29
|
+
### Uncontrolled
|
|
30
|
+
|
|
31
|
+
#### Before
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<Tabs defaultIndex={1}>
|
|
35
|
+
<TabList>
|
|
36
|
+
<Tab>Tab 1</Tab>
|
|
37
|
+
<Tab>Tab 2</Tab>
|
|
38
|
+
<Tab disabled>Disabled tab</Tab>
|
|
39
|
+
</TabList>
|
|
40
|
+
<TabPanels>
|
|
41
|
+
<TabPanel classNameOverride="p-4">Content 1</TabPanel>
|
|
42
|
+
<TabPanel>Content 2</TabPanel>
|
|
43
|
+
<TabPanel>Disabled content</TabPanel>
|
|
44
|
+
</TabPanels>
|
|
45
|
+
</Tabs>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### After
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<Tabs defaultSelectedKey="two">
|
|
52
|
+
<TabList>
|
|
53
|
+
<Tab id="one">Tab 1</Tab>
|
|
54
|
+
<Tab id="two">Tab 2</Tab>
|
|
55
|
+
<Tab id="three" isDisabled>Disabled tab</Tab>
|
|
56
|
+
</TabList>
|
|
57
|
+
<TabPanel id="one" className="p-4">Content 1</TabPanel>
|
|
58
|
+
<TabPanel id="two">Content 2</TabPanel>
|
|
59
|
+
</Tabs>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Controlled
|
|
63
|
+
|
|
64
|
+
#### Before
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Tabs onChange={setSelectedTab} defaultIndex={1}>
|
|
68
|
+
<TabList>
|
|
69
|
+
<Tab>Tab 1</Tab>
|
|
70
|
+
<Tab>Tab 2</Tab>
|
|
71
|
+
<Tab disabled>Disabled tab</Tab>
|
|
72
|
+
</TabList>
|
|
73
|
+
<TabPanels>
|
|
74
|
+
<TabPanel>Content 1</TabPanel>
|
|
75
|
+
<TabPanel>Content 2</TabPanel>
|
|
76
|
+
<TabPanel>Disabled content</TabPanel>
|
|
77
|
+
</TabPanels>
|
|
78
|
+
</Tabs>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### After
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Tabs onSelectionChange={setSelectedTab} selectedKey="two">
|
|
85
|
+
<TabList>
|
|
86
|
+
<Tab id="one">Tab 1</Tab>
|
|
87
|
+
<Tab id="two">Tab 2</Tab>
|
|
88
|
+
<Tab id="three" isDisabled>Disabled tab</Tab>
|
|
89
|
+
</TabList>
|
|
90
|
+
<TabPanel id="one">Content 1</TabPanel>
|
|
91
|
+
<TabPanel id="two">Content 2</TabPanel>
|
|
92
|
+
</Tabs>
|
|
93
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Text } from "~components/Text"
|
|
4
|
+
import { Button } from "~components/__actions__/v2"
|
|
5
|
+
import { Tab, TabList, TabPanel, Tabs, Key } from "../index"
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Components/Tabs/Tabs (Future)",
|
|
9
|
+
component: Tabs,
|
|
10
|
+
args: {
|
|
11
|
+
children: (
|
|
12
|
+
<>
|
|
13
|
+
<TabList aria-label="Tabs">
|
|
14
|
+
<Tab id="one">Tab 1</Tab>
|
|
15
|
+
<Tab id="two">Tab 2</Tab>
|
|
16
|
+
<Tab id="three" badge="3">
|
|
17
|
+
Tab 3
|
|
18
|
+
</Tab>
|
|
19
|
+
<Tab id="four" isDisabled>
|
|
20
|
+
Disabled Tab
|
|
21
|
+
</Tab>
|
|
22
|
+
</TabList>
|
|
23
|
+
<TabPanel id="one" className="p-24">
|
|
24
|
+
<Text variant="body">Content 1</Text>
|
|
25
|
+
</TabPanel>
|
|
26
|
+
<TabPanel id="two" className="p-24">
|
|
27
|
+
<Text variant="body">Content 2</Text>
|
|
28
|
+
</TabPanel>
|
|
29
|
+
<TabPanel id="three" className="p-24">
|
|
30
|
+
<Text variant="body">Content 3</Text>
|
|
31
|
+
</TabPanel>
|
|
32
|
+
</>
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
} satisfies Meta<typeof Tabs>
|
|
36
|
+
|
|
37
|
+
export default meta
|
|
38
|
+
|
|
39
|
+
type Story = StoryObj<typeof meta>
|
|
40
|
+
|
|
41
|
+
export const Playground: Story = {
|
|
42
|
+
parameters: {
|
|
43
|
+
chromatic: { disable: false },
|
|
44
|
+
docs: {
|
|
45
|
+
canvas: {
|
|
46
|
+
sourceState: "shown",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
defaultSelectedKey: "one",
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
onSelectionChange: (key): void => console.log("Tab changed to ", key),
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Controlled: Story = {
|
|
58
|
+
render: args => {
|
|
59
|
+
const [selectedKey, setSelectedKey] = useState<Key>(0)
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<Tabs
|
|
63
|
+
{...args}
|
|
64
|
+
selectedKey={selectedKey}
|
|
65
|
+
onSelectionChange={setSelectedKey}
|
|
66
|
+
/>
|
|
67
|
+
<Button
|
|
68
|
+
label="Switch to tab 2"
|
|
69
|
+
onClick={(): void => setSelectedKey("two")}
|
|
70
|
+
/>
|
|
71
|
+
</>
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.tab {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
border: 2px solid transparent;
|
|
5
|
+
border-bottom: 0;
|
|
6
|
+
border-top-left-radius: var(--border-borderless-border-radius);
|
|
7
|
+
border-top-right-radius: var(--border-borderless-border-radius);
|
|
8
|
+
background: var(--color-white);
|
|
9
|
+
white-space: nowrap;
|
|
10
|
+
text-decoration: none;
|
|
11
|
+
padding: var(--spacing-md) var(--spacing-md);
|
|
12
|
+
margin: 0;
|
|
13
|
+
font-family: var(--typography-heading-4-font-family);
|
|
14
|
+
font-size: var(--typography-heading-4-font-size);
|
|
15
|
+
font-weight: var(--typography-heading-4-font-weight);
|
|
16
|
+
line-height: var(--typography-heading-4-line-height);
|
|
17
|
+
letter-spacing: var(--typography-heading-4-letter-spacing);
|
|
18
|
+
color: var(--color-purple-800);
|
|
19
|
+
|
|
20
|
+
&:focus {
|
|
21
|
+
outline: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&:focus-visible {
|
|
25
|
+
background: var(--color-blue-100);
|
|
26
|
+
color: var(--color-blue-500);
|
|
27
|
+
border-color: var(--color-blue-500);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&[data-disabled] {
|
|
31
|
+
opacity: 0.3;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&:not(:first-child) {
|
|
35
|
+
margin-inline-start: var(--spacing-xs);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:not([data-disabled]):hover {
|
|
39
|
+
background: var(--color-blue-100);
|
|
40
|
+
color: var(--color-blue-500);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.tab[data-selected] {
|
|
45
|
+
position: relative;
|
|
46
|
+
color: var(--color-blue-500);
|
|
47
|
+
|
|
48
|
+
&::before {
|
|
49
|
+
content: "";
|
|
50
|
+
display: block;
|
|
51
|
+
border-top-left-radius: 5px;
|
|
52
|
+
border-top-right-radius: 5px;
|
|
53
|
+
background-color: currentcolor;
|
|
54
|
+
height: 5px;
|
|
55
|
+
width: 100%;
|
|
56
|
+
position: absolute;
|
|
57
|
+
left: 0;
|
|
58
|
+
right: 0;
|
|
59
|
+
bottom: 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.badge {
|
|
64
|
+
margin-inline-start: var(--spacing-sm);
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (forced-colors: active) {
|
|
70
|
+
.tab {
|
|
71
|
+
border: 2px solid transparent;
|
|
72
|
+
|
|
73
|
+
&:focus-visible::after {
|
|
74
|
+
content: "";
|
|
75
|
+
position: absolute;
|
|
76
|
+
background: transparent;
|
|
77
|
+
border-radius: var(--border-focus-ring-border-radius);
|
|
78
|
+
border-width: var(--border-focus-ring-border-width);
|
|
79
|
+
border-style: var(--border-focus-ring-border-style);
|
|
80
|
+
border-color: transparent;
|
|
81
|
+
inset: -2px;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.tab[data-selected]::before {
|
|
86
|
+
/* High contrast also doesn't see the pseudo element created to show the active tab. */
|
|
87
|
+
content: "";
|
|
88
|
+
position: absolute;
|
|
89
|
+
left: 0;
|
|
90
|
+
right: 0;
|
|
91
|
+
bottom: 0;
|
|
92
|
+
border-bottom: 2px solid transparent;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import classnames from "classnames"
|
|
3
|
+
import { Tab as RACTab, TabProps as RACTabProps } from "react-aria-components"
|
|
4
|
+
import { Badge } from "~components/Badge"
|
|
5
|
+
import styles from "./Tab.module.css"
|
|
6
|
+
|
|
7
|
+
export type TabProps = {
|
|
8
|
+
/**
|
|
9
|
+
* Adds a Kaizen Badge component to the tab.
|
|
10
|
+
* Comes with some logic baked in - changes variant based on active/focus/hover state.
|
|
11
|
+
*/
|
|
12
|
+
badge?: string
|
|
13
|
+
} & Omit<
|
|
14
|
+
RACTabProps,
|
|
15
|
+
// omitting link functionality because it goes against WAI ARIA standards https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
16
|
+
| "href"
|
|
17
|
+
| "hrefLang"
|
|
18
|
+
| "target"
|
|
19
|
+
| "rel"
|
|
20
|
+
| "download"
|
|
21
|
+
| "ping"
|
|
22
|
+
| "referrerPolicy"
|
|
23
|
+
>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A tab button
|
|
27
|
+
*/
|
|
28
|
+
export const Tab = (props: TabProps): JSX.Element => {
|
|
29
|
+
const { badge, children, className, ...restProps } = props
|
|
30
|
+
|
|
31
|
+
const tabProps = {
|
|
32
|
+
className: classnames(styles.tab, className),
|
|
33
|
+
...restProps,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<RACTab {...tabProps}>
|
|
38
|
+
{({ isSelected, isFocusVisible, isHovered }) => (
|
|
39
|
+
<>
|
|
40
|
+
{children}
|
|
41
|
+
{badge && (
|
|
42
|
+
<span className={styles.badge}>
|
|
43
|
+
<Badge
|
|
44
|
+
variant={
|
|
45
|
+
isSelected || isFocusVisible || isHovered
|
|
46
|
+
? "active"
|
|
47
|
+
: "default"
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
{badge}
|
|
51
|
+
</Badge>
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</RACTab>
|
|
57
|
+
)
|
|
58
|
+
}
|