@kaizen/components 1.68.10 → 1.68.12

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 (53) hide show
  1. package/dist/cjs/Tag/Tag.cjs +2 -33
  2. package/dist/cjs/Tag/Tag.module.scss.cjs +1 -8
  3. package/dist/cjs/Tag/subcomponents/LiveIcon/LiveIcon.cjs +56 -0
  4. package/dist/cjs/Tag/subcomponents/LiveIcon/LiveIcon.module.css.cjs +12 -0
  5. package/dist/cjs/Tile/TileGrid/TileGrid.cjs +19 -2
  6. package/dist/cjs/__future__/Tabs/constants.cjs +4 -0
  7. package/dist/cjs/__future__/Tabs/subcomponents/Tab/Tab.cjs +3 -1
  8. package/dist/cjs/__future__/Tabs/subcomponents/TabList/TabList.cjs +117 -3
  9. package/dist/cjs/__future__/Tabs/subcomponents/TabList/TabList.module.css.cjs +4 -1
  10. package/dist/cjs/__utilities__/isRTL/isRTL.cjs +11 -0
  11. package/dist/esm/Tag/Tag.mjs +2 -33
  12. package/dist/esm/Tag/Tag.module.scss.mjs +1 -8
  13. package/dist/esm/Tag/subcomponents/LiveIcon/LiveIcon.mjs +54 -0
  14. package/dist/esm/Tag/subcomponents/LiveIcon/LiveIcon.module.css.mjs +10 -0
  15. package/dist/esm/Tile/TileGrid/TileGrid.mjs +19 -2
  16. package/dist/esm/__future__/Tabs/constants.mjs +2 -0
  17. package/dist/esm/__future__/Tabs/subcomponents/Tab/Tab.mjs +3 -1
  18. package/dist/esm/__future__/Tabs/subcomponents/TabList/TabList.mjs +121 -5
  19. package/dist/esm/__future__/Tabs/subcomponents/TabList/TabList.module.css.mjs +4 -1
  20. package/dist/esm/__utilities__/isRTL/isRTL.mjs +9 -0
  21. package/dist/styles.css +155 -87
  22. package/dist/types/Tag/subcomponents/LiveIcon/LiveIcon.d.ts +4 -0
  23. package/dist/types/Tag/subcomponents/LiveIcon/index.d.ts +1 -0
  24. package/dist/types/Tag/subcomponents/index.d.ts +1 -0
  25. package/dist/types/Tile/TileGrid/TileGrid.d.ts +1 -1
  26. package/dist/types/__future__/Tabs/constants.d.ts +1 -0
  27. package/dist/types/__future__/Tabs/subcomponents/TabList/TabList.d.ts +1 -0
  28. package/dist/types/__utilities__/isRTL/index.d.ts +1 -0
  29. package/dist/types/__utilities__/isRTL/isRTL.d.ts +5 -0
  30. package/package.json +3 -3
  31. package/src/Tag/Tag.module.scss +0 -92
  32. package/src/Tag/Tag.tsx +2 -37
  33. package/src/Tag/subcomponents/LiveIcon/LiveIcon.module.css +91 -0
  34. package/src/Tag/subcomponents/LiveIcon/LiveIcon.tsx +48 -0
  35. package/src/Tag/subcomponents/LiveIcon/index.ts +1 -0
  36. package/src/Tag/subcomponents/index.ts +1 -0
  37. package/src/Tile/TileGrid/TileGrid.module.scss +1 -0
  38. package/src/Tile/TileGrid/TileGrid.tsx +32 -7
  39. package/src/Tile/TileGrid/_docs/TileGrid.stickersheet.stories.tsx +40 -0
  40. package/src/Tile/TileGrid/_docs/TileGrid.stories.tsx +78 -1
  41. package/src/Workflow/_docs/ProgressStepper.stickersheet.stories.tsx +59 -0
  42. package/src/Workflow/subcomponents/Footer/components/ProgressStepper/ProgressStepper.module.css +6 -0
  43. package/src/__future__/Tabs/_docs/Tabs.spec.stories.tsx +118 -0
  44. package/src/__future__/Tabs/_docs/Tabs.stickersheet.stories.tsx +84 -0
  45. package/src/__future__/Tabs/_docs/Tabs.stories.tsx +12 -1
  46. package/src/__future__/Tabs/constants.ts +1 -0
  47. package/src/__future__/Tabs/subcomponents/Tab/Tab.tsx +1 -1
  48. package/src/__future__/Tabs/subcomponents/TabList/TabList.module.css +53 -1
  49. package/src/__future__/Tabs/subcomponents/TabList/TabList.tsx +138 -10
  50. package/src/__future__/Tag/Tag/_docs/Tag-migration-guide.stories.tsx +24 -64
  51. package/src/__utilities__/isRTL/index.ts +1 -0
  52. package/src/__utilities__/isRTL/isRTL.spec.tsx +38 -0
  53. package/src/__utilities__/isRTL/isRTL.ts +6 -0
@@ -1,8 +1,60 @@
1
+ .container {
2
+ position: relative;
3
+ }
4
+
1
5
  .tabList {
2
6
  border-bottom: 1px solid rgba(var(--color-gray-600-rgb), 0.1);
3
- padding: var(--spacing-xs) var(--spacing-md) 0;
7
+ padding: var(--spacing-6) 0 0;
8
+ width: 100%;
9
+ height: 100%;
10
+ overflow-x: scroll;
11
+ white-space: nowrap;
12
+ scrollbar-width: none;
13
+ scroll-behavior: smooth;
4
14
  }
5
15
 
6
16
  .noPadding {
7
17
  padding: 0;
8
18
  }
19
+
20
+ .leftArrow,
21
+ .rightArrow {
22
+ --icon-size: 24;
23
+
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ position: absolute;
28
+ z-index: 10000;
29
+ background: var(--color-white);
30
+ inset-block: 0 1px;
31
+ width: 48px;
32
+ cursor: default;
33
+ user-select: none;
34
+ }
35
+
36
+ /*
37
+ * Note: we're purposefully using directional properties instead of start/end for positioning and styling related to the carousel arrows
38
+ */
39
+ .leftArrow {
40
+ left: 0;
41
+ }
42
+
43
+ .leftArrow,
44
+ .leftArrow:hover {
45
+ border-right: 1px solid rgba(var(--color-gray-600-rgb), 0.1);
46
+ }
47
+
48
+ .rightArrow {
49
+ right: 0;
50
+ }
51
+
52
+ .rightArrow,
53
+ .rightArrow:hover {
54
+ border-left: 1px solid rgba(var(--color-gray-600-rgb), 0.1);
55
+ }
56
+
57
+ .leftArrow:hover,
58
+ .rightArrow:hover {
59
+ background: var(--color-gray-200);
60
+ }
@@ -1,6 +1,13 @@
1
- import React, { ReactNode } from 'react'
1
+ import React, { ReactNode, useContext, useEffect, useId, useRef, useState } from 'react'
2
2
  import classnames from 'classnames'
3
- import { TabList as RACTabList, TabListProps as RACTabListProps } from 'react-aria-components'
3
+ import {
4
+ TabList as RACTabList,
5
+ TabListProps as RACTabListProps,
6
+ TabListStateContext,
7
+ } from 'react-aria-components'
8
+ import { Icon } from '~components/__future__/Icon'
9
+ import { isRTL as isRTLCheck } from '~components/__utilities__/isRTL'
10
+ import { SCROLL_AMOUNT } from '../../constants'
4
11
  import styles from './TabList.module.css'
5
12
 
6
13
  export type TabListProps = {
@@ -13,20 +20,141 @@ export type TabListProps = {
13
20
  */
14
21
  'noPadding'?: boolean
15
22
  'children': ReactNode
23
+ 'data-testid'?: string
16
24
  } & RACTabListProps<HTMLElement>
17
25
 
18
26
  /**
19
27
  * Wrapper for the tabs themselves
20
28
  */
21
29
  export const TabList = (props: TabListProps): JSX.Element => {
22
- const { 'aria-label': ariaLabel, noPadding = false, children, className, ...restProps } = props
30
+ const {
31
+ 'aria-label': ariaLabel,
32
+ noPadding = false,
33
+ children,
34
+ className,
35
+ 'data-testid': testId,
36
+ ...restProps
37
+ } = props
38
+ const [isDocumentReady, setIsDocumentReady] = useState<boolean>(false)
39
+ const [leftArrowEnabled, setLeftArrowEnabled] = useState<boolean>(false)
40
+ const [rightArrowEnabled, setRightArrowEnabled] = useState<boolean>(false)
41
+ const tabListRef = useRef<HTMLDivElement | null>(null)
42
+ const tabListId = useId()
43
+ const [isRTL, setIsRTL] = useState<boolean>(false)
44
+ const [containerElement, setContainerElement] = useState<HTMLElement | null>()
45
+ const tabListContext = useContext(TabListStateContext)
46
+ const selectedKey = tabListContext?.selectedKey
47
+
48
+ useEffect(() => {
49
+ if (!isDocumentReady) {
50
+ setIsDocumentReady(true)
51
+ return
52
+ }
53
+
54
+ const container = document.getElementById(tabListId)
55
+ setContainerElement(container)
56
+ setIsRTL(container ? isRTLCheck(container) : false)
57
+ }, [isDocumentReady, tabListId])
58
+
59
+ useEffect(() => {
60
+ if (!isDocumentReady) {
61
+ return
62
+ }
63
+
64
+ const tabs = containerElement?.querySelectorAll('[data-kz-tab]')
65
+ if (!tabs) {
66
+ return
67
+ }
68
+
69
+ const firstTabObserver = new IntersectionObserver(
70
+ (entries) => {
71
+ if (!entries[0].isIntersecting) {
72
+ setLeftArrowEnabled(true)
73
+ return
74
+ }
75
+ setLeftArrowEnabled(false)
76
+ },
77
+ {
78
+ threshold: 0.75,
79
+ root: containerElement,
80
+ },
81
+ )
82
+ firstTabObserver.observe(isRTL ? tabs[tabs.length - 1] : tabs[0])
83
+
84
+ const lastTabObserver = new IntersectionObserver(
85
+ (entries) => {
86
+ if (!entries[0].isIntersecting) {
87
+ setRightArrowEnabled(true)
88
+ return
89
+ }
90
+ setRightArrowEnabled(false)
91
+ },
92
+ {
93
+ threshold: 0.75,
94
+ root: containerElement,
95
+ },
96
+ )
97
+ lastTabObserver.observe(isRTL ? tabs[0] : tabs[tabs.length - 1])
98
+
99
+ return () => {
100
+ firstTabObserver.disconnect()
101
+ lastTabObserver.disconnect()
102
+ }
103
+ }, [isDocumentReady, containerElement, isRTL])
104
+
105
+ useEffect(() => {
106
+ if (!isDocumentReady) {
107
+ return
108
+ }
109
+
110
+ // Scroll selected tab into view
111
+ containerElement
112
+ ?.querySelector('[role="tab"][data-selected=true]')
113
+ ?.scrollIntoView({ block: 'nearest', inline: 'center' })
114
+ }, [selectedKey, containerElement, isDocumentReady])
115
+
116
+ const handleArrowPress = (direction: 'left' | 'right'): void => {
117
+ if (tabListRef.current) {
118
+ const tabListScrollPos = tabListRef.current.scrollLeft
119
+ const newSpot =
120
+ direction === 'left' ? tabListScrollPos - SCROLL_AMOUNT : tabListScrollPos + SCROLL_AMOUNT
121
+ tabListRef.current.scrollLeft = newSpot
122
+ }
123
+ }
124
+
23
125
  return (
24
- <RACTabList
25
- aria-label={ariaLabel}
26
- className={classnames(styles.tabList, className, noPadding && styles.noPadding)}
27
- {...restProps}
28
- >
29
- {children}
30
- </RACTabList>
126
+ <div className={styles.container} id={tabListId}>
127
+ {leftArrowEnabled && (
128
+ // making a conscious decision to use <div onClick> over <button> here, because:
129
+ // - <button> would add pointless noise for a screen reader user
130
+ // - keyboard only user can toggle through tabs with left/right arrow keys already
131
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
132
+ <div
133
+ onClick={() => handleArrowPress('left')}
134
+ className={styles.leftArrow}
135
+ data-testid={testId ? `${testId}-kz-tablist-left-arrow` : undefined}
136
+ >
137
+ <Icon name="chevron_left" isPresentational />
138
+ </div>
139
+ )}
140
+ <RACTabList
141
+ aria-label={ariaLabel}
142
+ ref={tabListRef}
143
+ className={classnames(styles.tabList, className, noPadding && styles.noPadding)}
144
+ {...restProps}
145
+ >
146
+ {children}
147
+ </RACTabList>
148
+ {rightArrowEnabled && (
149
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
150
+ <div
151
+ onClick={() => handleArrowPress('right')}
152
+ className={styles.rightArrow}
153
+ data-testid={testId ? `${testId}-kz-tablist-right-arrow` : undefined}
154
+ >
155
+ <Icon name="chevron_right" isPresentational />
156
+ </div>
157
+ )}
158
+ </div>
31
159
  )
32
160
  }
@@ -2,8 +2,7 @@ import React from 'react'
2
2
  import { Meta, StoryObj } from '@storybook/react'
3
3
  import { fn } from '@storybook/test'
4
4
  import { Avatar } from '~components/Avatar'
5
- import { LiveIcon } from '~components/Icon'
6
- import styles from '~components/Tag/Tag.module.scss'
5
+ import { LiveIcon } from '~components/Tag/subcomponents'
7
6
  import { Icon } from '~components/__future__/Icon'
8
7
  import { Tag, RemovableTag } from '../..'
9
8
 
@@ -23,8 +22,16 @@ const meta = {
23
22
  } satisfies Meta<typeof Tag>
24
23
 
25
24
  export default meta
25
+ type Story = StoryObj<typeof meta>
26
26
 
27
- /** * This is a stand-in component for the legacy Tag's bake in LiveIcon - we should consider adding this as an actual component or replacing it */
27
+ export const LiveIconComponentStory: Story = {
28
+ render: () => <LiveIcon />,
29
+ parameters: {
30
+ docs: {
31
+ source: {
32
+ type: 'dynamic',
33
+ code: `
34
+ // component with styled with CSS modules
28
35
  const LiveIconComponent = (): JSX.Element => (
29
36
  <span className={styles.liveIcon}>
30
37
  <LiveIcon
@@ -61,68 +68,21 @@ const LiveIconComponent = (): JSX.Element => (
61
68
  />
62
69
  </span>
63
70
  )
64
-
65
- export const LiveIconComponentStory: StoryObj = {
66
- render: () => <LiveIconComponent />,
67
- parameters: {
68
- docs: {
69
- source: {
70
- type: 'dynamic',
71
- code: `
72
- // component with styled with CSS modules
73
- const LiveIconComponent = (): JSX.Element => (
74
- <span className={styles.liveIcon}>
75
- <LiveIcon
76
- role="presentation"
77
- classNameOverride={styles.liveIcon_base}
78
- width="16"
79
- height="16"
80
- viewBox="0 0 16 16"
81
- fill="none"
82
- />
83
- <LiveIcon
84
- role="presentation"
85
- classNameOverride={styles.liveIcon_1}
86
- width="16"
87
- height="16"
88
- viewBox="0 0 16 16"
89
- fill="none"
90
- />
91
- <LiveIcon
92
- role="presentation"
93
- classNameOverride={styles.liveIcon_2}
94
- width="16"
95
- height="16"
96
- viewBox="0 0 16 16"
97
- fill="none"
98
- />
99
- <LiveIcon
100
- role="presentation"
101
- classNameOverride={styles.liveIcon_3}
102
- width="16"
103
- height="16"
104
- viewBox="0 0 16 16"
105
- fill="none"
106
- />
107
- </span>
108
- )
109
-
110
- // Minified SCSS from the stylesheet
111
- <style>
112
- .liveIcon_2,.liveIcon_3{animation-duration:3s;animation-iteration-count:3;animation-delay:1s}.liveIcon{display:inline-block;position:relative;width:20px;height:20px;color:$color-green-500}.liveIcon_1,.liveIcon_2,.liveIcon_3{display:block;position:absolute;top:0;left:$0;width:100%;height:100%;overflow:hidden}.liveIcon_base{opacity:30%;display:block}.liveIcon_1{clip-path:circle(16%)}.liveIcon_2{clip-path:circle(32%);animation-name:pulse-inner}.liveIcon_3{clip-path:circle(50%);animation-name:pulse-outer}@keyframes pulse-inner{0%,25%{opacity:0%}100%,50%,75%{opacity:100%}}@keyframes pulse-outer{0%,25%,50%{opacity:0%}100%,75%{opacity:100%}}
113
- </style>
114
- `,
71
+ // Minified SCSS from the stylesheet
72
+ <style>
73
+ .liveIcon_2,.liveIcon_3{animation-duration:3s;animation-iteration-count:3;animation-delay:1s}.liveIcon{display:inline-block;position:relative;width:20px;height:20px;color:$color-green-500}.liveIcon_1,.liveIcon_2,.liveIcon_3{display:block;position:absolute;top:0;left:$0;width:100%;height:100%;overflow:hidden}.liveIcon_base{opacity:30%;display:block}.liveIcon_1{clip-path:circle(16%)}.liveIcon_2{clip-path:circle(32%);animation-name:pulse-inner}.liveIcon_3{clip-path:circle(50%);animation-name:pulse-outer}@keyframes pulse-inner{0%,25%{opacity:0%}100%,50%,75%{opacity:100%}}@keyframes pulse-outer{0%,25%,50%{opacity:0%}100%,75%{opacity:100%}}
74
+ </style>`,
115
75
  },
116
76
  },
117
77
  },
118
78
  }
119
79
 
120
- export const StatusMigration: StoryObj = {
80
+ export const StatusMigration: Story = {
121
81
  render: () => (
122
82
  <>
123
83
  <Tag classNameOverride="gap-4" color="green">
124
84
  <span>Tag</span>
125
- <LiveIconComponent />
85
+ <LiveIcon />
126
86
  </Tag>
127
87
  <Tag color="blue">Tag</Tag>
128
88
  <Tag color="red">Tag</Tag>
@@ -138,7 +98,7 @@ export const StatusMigration: StoryObj = {
138
98
  ],
139
99
  }
140
100
 
141
- export const ValidationMigration: StoryObj = {
101
+ export const ValidationMigration: Story = {
142
102
  render: () => (
143
103
  <>
144
104
  <Tag color="green" icon={<Icon name="check_circle" isFilled alt="Success," />}>
@@ -164,7 +124,7 @@ export const ValidationMigration: StoryObj = {
164
124
  ],
165
125
  }
166
126
 
167
- export const SentimentsMigration: StoryObj = {
127
+ export const SentimentsMigration: Story = {
168
128
  render: () => (
169
129
  <>
170
130
  <Tag color="green">Tag</Tag>
@@ -184,7 +144,7 @@ export const SentimentsMigration: StoryObj = {
184
144
  ],
185
145
  }
186
146
 
187
- export const SentimentNone: StoryObj = {
147
+ export const SentimentNone: Story = {
188
148
  render: () => (
189
149
  <Tag color="gray" classNameOverride="bg-white border-default-color border-solid border">
190
150
  Tag
@@ -192,7 +152,7 @@ export const SentimentNone: StoryObj = {
192
152
  ),
193
153
  }
194
154
 
195
- export const DismissibleMigration: StoryObj = {
155
+ export const DismissibleMigration: Story = {
196
156
  render: () => (
197
157
  <RemovableTag
198
158
  removeButtonProps={{
@@ -205,7 +165,7 @@ export const DismissibleMigration: StoryObj = {
205
165
  ),
206
166
  }
207
167
 
208
- export const AvatarMigration: StoryObj = {
168
+ export const AvatarMigration: Story = {
209
169
  render: () => (
210
170
  <>
211
171
  <Tag classNameOverride="ps-4">
@@ -241,7 +201,7 @@ export const AvatarMigration: StoryObj = {
241
201
  ],
242
202
  }
243
203
 
244
- export const AvatarRemovableMigration: StoryObj = {
204
+ export const AvatarRemovableMigration: Story = {
245
205
  render: () => (
246
206
  <>
247
207
  <RemovableTag
@@ -295,7 +255,7 @@ export const AvatarRemovableMigration: StoryObj = {
295
255
  ],
296
256
  }
297
257
 
298
- export const InlineMigration: StoryObj = {
258
+ export const InlineMigration: Story = {
299
259
  render: () => (
300
260
  <div className="flex gap-12">
301
261
  <Tag>Tag</Tag>
@@ -305,4 +265,4 @@ export const InlineMigration: StoryObj = {
305
265
  ),
306
266
  }
307
267
 
308
- export const SizesMigration: StoryObj = {}
268
+ export const SizesMigration: Story = {}
@@ -0,0 +1 @@
1
+ export * from './isRTL'
@@ -0,0 +1,38 @@
1
+ import React from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { isRTL } from './isRTL'
4
+
5
+ describe('isRTL', () => {
6
+ it('returns false when no element with dir found', () => {
7
+ const Example = (): JSX.Element => <button type="button">Test</button>
8
+ render(<Example />)
9
+ const button = screen.getByRole('button')
10
+ expect(isRTL(button)).toBe(false)
11
+ })
12
+
13
+ it('returns false when greater parent is dir=rtl, but closer parent is dir=ltr', () => {
14
+ const Example = (): JSX.Element => (
15
+ <div dir="rtl">
16
+ <div dir="ltr">
17
+ <button type="button">Test</button>
18
+ </div>
19
+ </div>
20
+ )
21
+ render(<Example />)
22
+ const button = screen.getByRole('button')
23
+ expect(isRTL(button)).toBe(false)
24
+ })
25
+
26
+ it('returns true when greater parent is dir=ltr, but closer parent is dir=rtl', () => {
27
+ const Example = (): JSX.Element => (
28
+ <div dir="ltr">
29
+ <div dir="rtl">
30
+ <button type="button">Test</button>
31
+ </div>
32
+ </div>
33
+ )
34
+ render(<Example />)
35
+ const button = screen.getByRole('button')
36
+ expect(isRTL(button)).toBe(true)
37
+ })
38
+ })
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Finds the first ancestor with a `dir` property on it
3
+ * Returning true is that is `dir=rtl` and returning false in all other cases
4
+ */
5
+ export const isRTL = (element: Element): boolean =>
6
+ !!element.closest('[dir]')?.matches('[dir="rtl"]')