@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.
Files changed (64) hide show
  1. package/dist/cjs/Loading/LoadingSpinner/LoadingSpinner.cjs +6 -42
  2. package/dist/cjs/Loading/LoadingSpinner/LoadingSpinner.module.css.cjs +10 -0
  3. package/dist/cjs/Loading/LoadingSpinner/subcomponents/SpinnerIcon.cjs +80 -0
  4. package/dist/cjs/__future__/Tabs/Tabs.cjs +23 -0
  5. package/dist/cjs/__future__/Tabs/subcomponents/Tab/Tab.cjs +39 -0
  6. package/dist/cjs/__future__/Tabs/subcomponents/Tab/Tab.module.css.cjs +7 -0
  7. package/dist/cjs/__future__/Tabs/subcomponents/TabList/TabList.cjs +31 -0
  8. package/dist/cjs/__future__/Tabs/subcomponents/TabList/TabList.module.css.cjs +7 -0
  9. package/dist/cjs/__future__/Tabs/subcomponents/TabPanel/TabPanel.cjs +24 -0
  10. package/dist/cjs/future.cjs +8 -0
  11. package/dist/esm/Loading/LoadingSpinner/LoadingSpinner.mjs +6 -42
  12. package/dist/esm/Loading/LoadingSpinner/LoadingSpinner.module.css.mjs +8 -0
  13. package/dist/esm/Loading/LoadingSpinner/subcomponents/SpinnerIcon.mjs +72 -0
  14. package/dist/esm/__future__/Tabs/Tabs.mjs +15 -0
  15. package/dist/esm/__future__/Tabs/subcomponents/Tab/Tab.mjs +30 -0
  16. package/dist/esm/__future__/Tabs/subcomponents/Tab/Tab.module.css.mjs +5 -0
  17. package/dist/esm/__future__/Tabs/subcomponents/TabList/TabList.mjs +22 -0
  18. package/dist/esm/__future__/Tabs/subcomponents/TabList/TabList.module.css.mjs +5 -0
  19. package/dist/esm/__future__/Tabs/subcomponents/TabPanel/TabPanel.mjs +16 -0
  20. package/dist/esm/future.mjs +4 -0
  21. package/dist/styles.css +246 -110
  22. package/dist/types/Loading/LoadingSpinner/LoadingSpinner.d.ts +2 -2
  23. package/dist/types/Loading/LoadingSpinner/subcomponents/SpinnerIcon.d.ts +5 -0
  24. package/dist/types/Loading/LoadingSpinner/subcomponents/index.d.ts +1 -0
  25. package/dist/types/Tabs/subcomponents/index.d.ts +0 -1
  26. package/dist/types/__future__/Tabs/Tabs.d.ts +11 -0
  27. package/dist/types/__future__/Tabs/index.d.ts +2 -0
  28. package/dist/types/__future__/Tabs/subcomponents/Tab/Tab.d.ts +12 -0
  29. package/dist/types/__future__/Tabs/subcomponents/Tab/index.d.ts +1 -0
  30. package/dist/types/__future__/Tabs/subcomponents/TabList/TabList.d.ts +17 -0
  31. package/dist/types/__future__/Tabs/subcomponents/TabList/index.d.ts +1 -0
  32. package/dist/types/__future__/Tabs/subcomponents/TabPanel/TabPanel.d.ts +6 -0
  33. package/dist/types/__future__/Tabs/subcomponents/TabPanel/index.d.ts +1 -0
  34. package/dist/types/__future__/Tabs/subcomponents/index.d.ts +3 -0
  35. package/dist/types/__future__/index.d.ts +1 -0
  36. package/package.json +2 -2
  37. package/src/Loading/LoadingSpinner/LoadingSpinner.module.css +32 -0
  38. package/src/Loading/LoadingSpinner/LoadingSpinner.tsx +10 -54
  39. package/src/Loading/LoadingSpinner/_docs/LoadingSpinner.mdx +7 -2
  40. package/src/Loading/LoadingSpinner/_docs/LoadingSpinner.stickersheet.stories.tsx +2 -1
  41. package/src/Loading/LoadingSpinner/_docs/LoadingSpinner.stories.tsx +19 -0
  42. package/src/Loading/LoadingSpinner/subcomponents/SpinnerIcon.tsx +87 -0
  43. package/src/Loading/LoadingSpinner/subcomponents/index.ts +1 -0
  44. package/src/Tabs/subcomponents/index.ts +0 -1
  45. package/src/__actions__/Button/v3/_docs/Button.stickersheet.stories.tsx +2 -2
  46. package/src/__future__/Tabs/Tabs.tsx +18 -0
  47. package/src/__future__/Tabs/_docs/Tabs--api-specification.mdx +43 -0
  48. package/src/__future__/Tabs/_docs/Tabs--migration-guide.mdx +93 -0
  49. package/src/__future__/Tabs/_docs/Tabs.stories.tsx +74 -0
  50. package/src/__future__/Tabs/index.ts +2 -0
  51. package/src/__future__/Tabs/subcomponents/Tab/Tab.module.css +94 -0
  52. package/src/__future__/Tabs/subcomponents/Tab/Tab.tsx +58 -0
  53. package/src/__future__/Tabs/subcomponents/Tab/index.ts +1 -0
  54. package/src/__future__/Tabs/subcomponents/TabList/TabList.module.css +8 -0
  55. package/src/__future__/Tabs/subcomponents/TabList/TabList.tsx +45 -0
  56. package/src/__future__/Tabs/subcomponents/TabList/index.ts +1 -0
  57. package/src/__future__/Tabs/subcomponents/TabPanel/TabPanel.module.css +12 -0
  58. package/src/__future__/Tabs/subcomponents/TabPanel/TabPanel.tsx +20 -0
  59. package/src/__future__/Tabs/subcomponents/TabPanel/index.ts +1 -0
  60. package/src/__future__/Tabs/subcomponents/index.ts +3 -0
  61. package/src/__future__/index.ts +1 -0
  62. package/dist/cjs/Loading/LoadingSpinner/LoadingSpinner.module.scss.cjs +0 -7
  63. package/dist/esm/Loading/LoadingSpinner/LoadingSpinner.module.scss.mjs +0 -5
  64. 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 styles from "./LoadingSpinner.module.scss"
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
- data-automation-id="loading-spinner"
27
- className={classnames(styles.loadingSpinner, classNameOverride)}
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 === "md" ? (
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 colour"]}
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"
@@ -2,4 +2,3 @@ export * from "./Tab"
2
2
  export * from "./TabList"
3
3
  export * from "./TabPanel"
4
4
  export * from "./TabPanels"
5
- export * from "./Tab"
@@ -82,7 +82,7 @@ const StickerSheetTemplate: StickerSheetStory = {
82
82
  <ArrowForwardIcon role="presentation" />
83
83
  </Button>
84
84
  <Button size="small" isDisabled>
85
- <LoadingSpinner size="sm" accessibilityLabel="submitting label" />
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="sm" accessibilityLabel="Removing label" />
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,2 @@
1
+ export * from "./Tabs"
2
+ export * from "./subcomponents"
@@ -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
+ }