@kaizen/components 0.0.0-canary-v2-20250901045936 → 0.0.0-canary-debug-tab-20251015223744

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 (165) hide show
  1. package/alpha/README.md +28 -0
  2. package/alpha/package.json +5 -0
  3. package/dist/cjs/alpha.cjs +1 -0
  4. package/dist/cjs/src/Modal/GenericModal/GenericModal.cjs +33 -65
  5. package/dist/cjs/src/Modal/GenericModal/GenericModal.module.scss.cjs +1 -3
  6. package/dist/cjs/src/Notification/InlineNotification/InlineNotification.cjs +1 -1
  7. package/dist/cjs/src/SingleSelect/subcomponents/ListBox/ListBox.cjs +6 -2
  8. package/dist/cjs/src/Tabs/subcomponents/TabList/TabList.cjs +29 -21
  9. package/dist/cjs/src/__alpha__/SingleSelect/SingleSelect.cjs +35 -74
  10. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.cjs +105 -0
  11. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css.cjs +11 -0
  12. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.cjs +112 -0
  13. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css.cjs +16 -0
  14. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/List/List.cjs +35 -10
  15. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.cjs +61 -8
  16. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css.cjs +10 -1
  17. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.cjs +38 -9
  18. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css.cjs +4 -1
  19. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.cjs +60 -30
  20. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.cjs +2 -1
  21. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.cjs +2 -1
  22. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.cjs +4 -2
  23. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Select/Select.cjs +87 -0
  24. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css.cjs +11 -0
  25. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.cjs +52 -0
  26. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css.cjs +13 -0
  27. package/dist/esm/alpha.mjs +1 -1
  28. package/dist/esm/src/Modal/GenericModal/GenericModal.mjs +34 -65
  29. package/dist/esm/src/Modal/GenericModal/GenericModal.module.scss.mjs +1 -3
  30. package/dist/esm/src/Notification/InlineNotification/InlineNotification.mjs +1 -1
  31. package/dist/esm/src/SingleSelect/subcomponents/ListBox/ListBox.mjs +6 -2
  32. package/dist/esm/src/Tabs/subcomponents/TabList/TabList.mjs +29 -21
  33. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.mjs +39 -73
  34. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.mjs +96 -0
  35. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css.mjs +9 -0
  36. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.mjs +103 -0
  37. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css.mjs +14 -0
  38. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/List/List.mjs +37 -14
  39. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.mjs +63 -13
  40. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css.mjs +10 -1
  41. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.mjs +41 -15
  42. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css.mjs +4 -1
  43. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.mjs +69 -43
  44. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.mjs +2 -1
  45. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.mjs +2 -1
  46. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.mjs +4 -2
  47. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Select/Select.mjs +78 -0
  48. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css.mjs +9 -0
  49. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.mjs +43 -0
  50. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css.mjs +11 -0
  51. package/dist/styles.css +443 -79
  52. package/dist/types/__alpha__/SingleSelect/SingleSelect.d.ts +14 -19
  53. package/dist/types/__alpha__/SingleSelect/_docs/mockData.d.ts +3 -0
  54. package/dist/types/__alpha__/SingleSelect/context/SingleSelectContext.d.ts +15 -7
  55. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.d.ts +2 -0
  56. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBox/index.d.ts +1 -0
  57. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.d.ts +2 -0
  58. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/index.d.ts +1 -0
  59. package/dist/types/__alpha__/SingleSelect/subcomponents/List/List.d.ts +2 -7
  60. package/dist/types/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.d.ts +2 -7
  61. package/dist/types/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.d.ts +2 -9
  62. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/Popover.d.ts +3 -6
  63. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/index.d.ts +1 -0
  64. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.d.ts +1 -0
  65. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.d.ts +1 -0
  66. package/dist/types/__alpha__/SingleSelect/subcomponents/Select/Select.d.ts +2 -0
  67. package/dist/types/__alpha__/SingleSelect/subcomponents/Select/index.d.ts +1 -0
  68. package/dist/types/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.d.ts +2 -0
  69. package/dist/types/__alpha__/SingleSelect/subcomponents/SelectTrigger/index.d.ts +1 -0
  70. package/dist/types/__alpha__/SingleSelect/subcomponents/index.d.ts +4 -1
  71. package/dist/types/__alpha__/SingleSelect/types.d.ts +68 -11
  72. package/locales/ar.json +9 -1
  73. package/locales/bg.json +9 -1
  74. package/locales/cs.json +9 -1
  75. package/locales/cy.json +9 -1
  76. package/locales/da.json +9 -1
  77. package/locales/de.json +9 -1
  78. package/locales/el.json +9 -1
  79. package/locales/en-GB.json +9 -1
  80. package/locales/en.json +9 -1
  81. package/locales/es-419.json +9 -1
  82. package/locales/es.json +9 -1
  83. package/locales/et.json +9 -1
  84. package/locales/fi.json +9 -1
  85. package/locales/fr-CA.json +9 -1
  86. package/locales/fr.json +9 -1
  87. package/locales/he.json +9 -1
  88. package/locales/hi.json +9 -1
  89. package/locales/ht.json +9 -1
  90. package/locales/hu.json +9 -1
  91. package/locales/id.json +9 -1
  92. package/locales/it.json +9 -1
  93. package/locales/ja.json +9 -1
  94. package/locales/km-KH.json +9 -1
  95. package/locales/ko.json +9 -1
  96. package/locales/lt.json +9 -1
  97. package/locales/lv.json +9 -1
  98. package/locales/mi.json +10 -2
  99. package/locales/ms.json +9 -1
  100. package/locales/nb.json +9 -1
  101. package/locales/nl.json +9 -1
  102. package/locales/pl.json +9 -1
  103. package/locales/pt-BR.json +9 -1
  104. package/locales/pt.json +9 -1
  105. package/locales/ro.json +9 -1
  106. package/locales/ru.json +9 -1
  107. package/locales/si-LK.json +9 -1
  108. package/locales/sk.json +9 -1
  109. package/locales/sr.json +9 -1
  110. package/locales/sv.json +9 -1
  111. package/locales/th.json +9 -1
  112. package/locales/tl.json +9 -1
  113. package/locales/tr.json +9 -1
  114. package/locales/uk.json +9 -1
  115. package/locales/vi.json +9 -1
  116. package/locales/zh-TW.json +9 -1
  117. package/locales/zh.json +9 -1
  118. package/package.json +10 -3
  119. package/src/Modal/GenericModal/GenericModal.spec.tsx +1 -1
  120. package/src/Modal/GenericModal/GenericModal.tsx +38 -70
  121. package/src/Notification/InlineNotification/InlineNotification.tsx +1 -1
  122. package/src/RichTextEditor/RichTextEditor/RichTextEditor.spec.stories.tsx +10 -3
  123. package/src/SingleSelect/subcomponents/ListBox/ListBox.tsx +2 -2
  124. package/src/Tabs/subcomponents/TabList/TabList.tsx +40 -30
  125. package/src/__alpha__/SingleSelect/SingleSelect.tsx +35 -88
  126. package/src/__alpha__/SingleSelect/_docs/SingleSelect.mdx +96 -6
  127. package/src/__alpha__/SingleSelect/_docs/SingleSelect.spec.stories.tsx +22 -24
  128. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stickersheet.stories.tsx +389 -33
  129. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stories.tsx +41 -22
  130. package/src/__alpha__/SingleSelect/_docs/mockData.ts +20 -14
  131. package/src/__alpha__/SingleSelect/context/SingleSelectContext.tsx +18 -7
  132. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css +35 -0
  133. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.tsx +106 -0
  134. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/index.ts +1 -0
  135. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css +130 -0
  136. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.tsx +121 -0
  137. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/index.ts +1 -0
  138. package/src/__alpha__/SingleSelect/subcomponents/List/List.module.css +5 -0
  139. package/src/__alpha__/SingleSelect/subcomponents/List/List.tsx +36 -13
  140. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css +84 -3
  141. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.tsx +67 -11
  142. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css +20 -5
  143. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.tsx +46 -19
  144. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css +7 -5
  145. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.tsx +90 -37
  146. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/index.ts +1 -0
  147. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.ts +2 -2
  148. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.ts +9 -8
  149. package/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css +35 -0
  150. package/src/__alpha__/SingleSelect/subcomponents/Select/Select.tsx +84 -0
  151. package/src/__alpha__/SingleSelect/subcomponents/Select/index.ts +1 -0
  152. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css +77 -0
  153. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.tsx +52 -0
  154. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/index.ts +1 -0
  155. package/src/__alpha__/SingleSelect/subcomponents/index.ts +4 -1
  156. package/src/__alpha__/SingleSelect/types.ts +94 -14
  157. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.cjs +0 -57
  158. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css.cjs +0 -6
  159. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.mjs +0 -49
  160. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css.mjs +0 -4
  161. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.d.ts +0 -2
  162. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/index.d.ts +0 -1
  163. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css +0 -19
  164. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.tsx +0 -35
  165. package/src/__alpha__/SingleSelect/subcomponents/Trigger/index.ts +0 -1
@@ -35,7 +35,7 @@ export const InlineNotification = forwardRef<HTMLDivElement, InlineNotificationP
35
35
  ): JSX.Element => (
36
36
  <GenericNotification
37
37
  style="inline"
38
- persistent={persistent ?? hideCloseIcon}
38
+ persistent={persistent || hideCloseIcon}
39
39
  classNameOverride={classnames(classNameOverride, [isSubtle && styles.subtle])}
40
40
  ref={ref}
41
41
  {...otherProps}
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
2
  import { type StoryObj } from '@storybook/react'
3
- import { expect, userEvent, within } from '@storybook/test'
3
+ import { expect, userEvent, waitFor, within } from '@storybook/test'
4
4
  import { type EditorContentArray } from '../types'
5
5
  import { RichTextEditor, type RichTextEditorProps } from './RichTextEditor'
6
6
 
@@ -149,8 +149,9 @@ export const CreateALink: Story = {
149
149
  name: 'Create a link',
150
150
  play: async (context) => {
151
151
  const { canvasElement, step } = context
152
- const { getByRole, getByText } = within(canvasElement)
152
+ const { getByRole, getByText, queryByRole, findByRole } = within(canvasElement)
153
153
  const editor = getByRole('textbox')
154
+
154
155
  await step('Focus on editor', async () => {
155
156
  await userEvent.click(editor)
156
157
  expect(editor).toHaveFocus()
@@ -185,8 +186,14 @@ export const CreateALink: Story = {
185
186
  await userEvent.keyboard('{Tab}{Enter}')
186
187
  })
187
188
 
189
+ await step('The Link Modal closes', async () => {
190
+ await waitFor(() => {
191
+ expect(queryByRole('dialog')).not.toBeInTheDocument()
192
+ })
193
+ })
194
+
188
195
  await step('Link exists in the RTE', async () => {
189
- const link = getByRole('link', { name: 'Link' })
196
+ const link = await findByRole('link', { name: 'Link' })
190
197
  expect(link).toBeInTheDocument()
191
198
  })
192
199
  },
@@ -70,10 +70,10 @@ export const ListBox = <Option extends SingleSelectOption>({
70
70
  const focusToElement = safeQuerySelector(`[data-key='${optionKey}']`)
71
71
 
72
72
  if (focusToElement) {
73
- focusToElement.focus()
73
+ focusToElement.focus({ preventScroll: true })
74
74
  } else {
75
75
  // If an element is not found, focus on the listbox. This ensures the list can still be navigated to via keyboard if the keys do not align to the data attributes of the list items.
76
- ref.current?.focus()
76
+ ref.current?.focus({ preventScroll: true })
77
77
  }
78
78
  }
79
79
  // Only run this effect for checking the first successful render
@@ -66,39 +66,49 @@ export const TabList = (props: TabListProps): JSX.Element => {
66
66
  return
67
67
  }
68
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.8,
79
- root: containerElement,
80
- },
81
- )
82
- firstTabObserver.observe(isRTL ? tabs[tabs.length - 1] : tabs[0])
69
+ let firstTabObserver: IntersectionObserver | null = null
70
+ let lastTabObserver: IntersectionObserver | null = null
83
71
 
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.8,
94
- root: containerElement,
95
- },
96
- )
97
- lastTabObserver.observe(isRTL ? tabs[0] : tabs[tabs.length - 1])
72
+ requestAnimationFrame(() => {
73
+ const tabList = tabListRef.current
74
+ if (!tabList) {
75
+ return
76
+ }
77
+
78
+ firstTabObserver = new IntersectionObserver(
79
+ (entries) => {
80
+ if (!entries[0].isIntersecting) {
81
+ setLeftArrowEnabled(true)
82
+ return
83
+ }
84
+ setLeftArrowEnabled(false)
85
+ },
86
+ {
87
+ threshold: 0.8,
88
+ root: containerElement,
89
+ },
90
+ )
91
+ firstTabObserver.observe(isRTL ? tabs[tabs.length - 1] : tabs[0])
92
+
93
+ lastTabObserver = new IntersectionObserver(
94
+ (entries) => {
95
+ if (!entries[0].isIntersecting) {
96
+ setRightArrowEnabled(true)
97
+ return
98
+ }
99
+ setRightArrowEnabled(false)
100
+ },
101
+ {
102
+ threshold: 0.8,
103
+ root: containerElement,
104
+ },
105
+ )
106
+ lastTabObserver.observe(isRTL ? tabs[0] : tabs[tabs.length - 1])
107
+ })
98
108
 
99
109
  return () => {
100
- firstTabObserver.disconnect()
101
- lastTabObserver.disconnect()
110
+ firstTabObserver?.disconnect()
111
+ lastTabObserver?.disconnect()
102
112
  }
103
113
  }, [isDocumentReady, containerElement, isRTL, tabListContext?.collection.size])
104
114
 
@@ -1,92 +1,39 @@
1
- import React, { cloneElement, isValidElement, useId, useMemo, type PropsWithChildren } from 'react'
2
- import { useSelectState } from '@react-stately/select'
3
- import { type Key, type Selection } from '@react-types/shared'
4
- import { Select as RACSelect, type ListBoxProps } from 'react-aria-components'
5
- import { SingleSelectContext } from './context'
6
- import { List, ListItem, ListSection, Popover, Trigger } from './subcomponents'
7
- import { type SelectItem, type SelectSection, type SingleSelectProps } from './types'
8
-
9
- export const SingleSelect = ({
10
- items,
11
- onSelectionChange,
12
- children,
13
- ...restProps
14
- }: PropsWithChildren<SingleSelectProps>): JSX.Element => {
15
- const buttonRef = React.useRef<HTMLButtonElement>(null)
16
- const popoverRef = React.useRef<HTMLDivElement>(null)
17
- const racPopoverRef = React.useRef<HTMLElement>(null)
18
- const uniqueId = useId()
19
- const anchorName = `--trigger-${uniqueId}`
20
-
21
- const state = useSelectState({
22
- items,
23
- })
24
-
25
- const handleOnSelectionChange = React.useCallback(
26
- (keys: Selection): void => {
27
- let key: Key | null = null
28
-
29
- if (keys instanceof Set && keys.size > 0) {
30
- key = Array.from(keys)[0]
31
- }
32
-
33
- state.setSelectedKey(key)
34
- if (onSelectionChange) {
35
- onSelectionChange(key)
36
- }
37
- },
38
- [state, onSelectionChange],
39
- )
40
-
41
- // Cloning children here to allow users to pass in a custom ListItem or ListSection
42
- // and still have the SingleSelect handle selection state
43
- const injectedChildren = useMemo(() => {
44
- if (!isValidElement(children)) return null
45
-
46
- const selectedKeys: Iterable<Key> = state.selectedKey
47
- ? new Set<Key>([state.selectedKey])
48
- : new Set()
1
+ import React from 'react'
2
+ import { Item as StatelyItem, Section } from '@react-stately/collections'
3
+ import { ComboBox, Select } from './subcomponents'
4
+ import {
5
+ type ComboBoxProps,
6
+ type SelectItem,
7
+ type SelectProps,
8
+ type SingleSelectProps,
9
+ } from './types'
10
+
11
+ export const SingleSelect = <T extends SelectItem>(props: SingleSelectProps<T>): JSX.Element => {
12
+ const { isComboBox, children, ...rest } = props
13
+
14
+ if (isComboBox) {
15
+ return <ComboBox {...(rest as ComboBoxProps<T>)}>{children}</ComboBox>
16
+ }
17
+
18
+ return <Select {...(rest as SelectProps<T>)}>{children}</Select>
19
+ }
49
20
 
50
- return cloneElement(children as React.ReactElement<ListBoxProps<SelectItem | SelectSection>>, {
51
- selectionMode: 'single',
52
- selectedKeys,
53
- onSelectionChange: handleOnSelectionChange,
54
- autoFocus: 'first',
55
- })
56
- }, [children, handleOnSelectionChange, state.selectedKey])
21
+ type CustomItemProps = {
22
+ selectedIcon?: 'check' | 'radio'
23
+ selectedPosition?: 'start' | 'end'
24
+ key: string
25
+ children?: React.ReactNode
26
+ [key: string]: any
27
+ }
57
28
 
58
- return (
59
- <SingleSelectContext.Provider
60
- value={{
61
- isOpen: state.isOpen,
62
- setOpen: state.setOpen,
63
- selectedKey: state.selectedKey,
64
- items: items,
65
- anchorName,
66
- }}
67
- >
68
- <RACSelect
69
- // TODO: allow user to pass in label
70
- aria-label={'single-select'}
71
- onSelectionChange={(key) =>
72
- handleOnSelectionChange(key != null ? new Set([key]) : new Set())
73
- }
74
- placeholder=""
75
- {...restProps}
76
- >
77
- <Trigger buttonRef={buttonRef} />
29
+ export const Item = React.forwardRef<HTMLElement, CustomItemProps>((props, ref) => {
30
+ // @ts-expect-error: StatelyItem doesn't know about our internal item props
31
+ return <StatelyItem {...props} ref={ref} />
32
+ })
78
33
 
79
- {state.isOpen && (
80
- <Popover buttonRef={buttonRef} popoverRef={popoverRef} racPopoverRef={racPopoverRef}>
81
- {injectedChildren}
82
- </Popover>
83
- )}
84
- </RACSelect>
85
- </SingleSelectContext.Provider>
86
- )
87
- }
34
+ // @ts-expect-error: doesn't know that the Item can have this static property
35
+ Item.getCollectionNode = StatelyItem.getCollectionNode
36
+ Item.displayName = 'SingleSelectItem'
88
37
 
89
- SingleSelect.displayName = 'SingleSelect'
90
- SingleSelect.List = List
91
- SingleSelect.ListItem = ListItem
92
- SingleSelect.ListSection = ListSection
38
+ SingleSelect.Item = Item
39
+ SingleSelect.Section = Section
@@ -1,14 +1,15 @@
1
1
  import { Canvas, Controls, DocsStory, Meta } from '@storybook/blocks'
2
2
  import { ResourceLinks, KAIOInstallation, AlphaNotice } from '~storybook/components'
3
- import * as SingleSelectStories from './SingleSelect.stories'
3
+ import { Playground } from './SingleSelect.stories'
4
+ import { SingleSelect } from '../index'
4
5
 
5
- <Meta of={SingleSelectStories} />
6
+ <Meta title="Components/SingleSelect/SingleSelect (alpha)" component={SingleSelect} />
6
7
 
7
8
  # SingleSelect
8
9
 
9
10
  <ResourceLinks
10
11
  sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/__alpha__/SingleSelect"
11
- figma=""
12
+ figma="https://www.figma.com/design/umhYV8q0yC4qwbCOIfJDY5/-Alpha--Select?node-id=2122-15597&t=EgUdLXABrcuWMIay-0"
12
13
  designGuidelines=""
13
14
  />
14
15
 
@@ -16,15 +17,104 @@ import * as SingleSelectStories from './SingleSelect.stories'
16
17
 
17
18
  <KAIOInstallation exportNames="SingleSelect" isAlpha />
18
19
 
20
+ ## Alpha notice
21
+
22
+ This component is currently in **alpha** and not yet fully feature-complete. The following capabilities are planned or in progress:
23
+
24
+ - Form validation and integration with form libraries
25
+ - Support for passing a custom defaultFilter function from React Aria
26
+ - Full support for asynchronous list loading and dynamic items
27
+ - Ability to use the component in a fully controlled manner (selectedKey, inputValue, etc.)
28
+ - Forwarding refs to the appropriate internal elements
29
+ - Enhanced composability beyond the provided Item and Section components
30
+
31
+ We welcome feedback as we continue to improve the component.
32
+
19
33
  ## Overview
20
34
 
21
- SingleSelect component that handles selecting items from a dropdown, can be either filterable or not.
35
+ The `SingleSelect` component allows users to select a single item from a dropdown list. It can be configured as a **Select** or **Combobox**. A Select provides a predefined list of options, while a Combobox adds a filterable input to help users find an option quickly.
22
36
 
23
- <Canvas of={SingleSelectStories.Playground} />
24
- <Controls of={SingleSelectStories.Playground} />
37
+ <Canvas of={Playground} />
38
+ <Controls of={Playground} />
25
39
 
26
40
  ## API
27
41
 
42
+ ## Combobox vs. Select
43
+
44
+ The `SingleSelect` component supports two different behaviors:
45
+
46
+ - **Select**: A classic dropdown menu where the user can only select from a predefined list of options. It's ideal for short, fixed lists.
47
+ - **Combobox**: An input field combined with a dropdown. The user can either type to filter the options or click to open the full list. This is best for long lists or when quick searching is a priority.
48
+
49
+ To use the Combobox behavior, simply add the `isComboBox` prop.
50
+
51
+ ```tsx
52
+ // Select
53
+ <SingleSelect label="Select an item">
54
+ <SingleSelect.Item key="1">Item 1</SingleSelect.Item>
55
+ <SingleSelect.Item key="2">Item 2</SingleSelect.Item>
56
+ </SingleSelect>
57
+
58
+ // Combobox
59
+ <SingleSelect label="Select an item" isComboBox>
60
+ <SingleSelect.Item key="1">Item 1</SingleSelect.Item>
61
+ <SingleSelect.Item key="2">Item 2</SingleSelect.Item>
62
+ </SingleSelect>
63
+ ```
64
+
65
+ ## Filtering
66
+
67
+ Children as static elements (leaves)
68
+
69
+ ```tsx
70
+ <SingleSelect label="Choose a coffee" isComboBox items={coffeeItems}>
71
+ {(item) => <SingleSelect.Item key={item.key}>{item.label}</SingleSelect.Item>}
72
+ </SingleSelect>
73
+ ```
74
+
75
+ Children as a render function + `items` prop
76
+
77
+ ```tsx
78
+ const [filterText, setFilterText] = useState('')
79
+
80
+ <SingleSelect
81
+ label="Choose a coffee"
82
+ isComboBox
83
+ items={coffeeItems.filter((item) => item.label.toLowerCase().includes(filterText.toLowerCase()))}
84
+ onInputChange={setFilterText}
85
+ >
86
+ {(item) => <SingleSelect.Item key={item.key} textValue={item.label}>{item.label}</SingleSelect.Item>}
87
+ </SingleSelect>
88
+ ```
89
+
90
+ ## Flexible Rendering of items
91
+
92
+ The SingleSelect.Item component is flexible and can accommodate a wide variety of content beyond simple text labels. You can render avatars, icons, or any custom React nodes within an item, while still preserving accessibility by providing the textValue prop.
93
+
94
+ ```tsx
95
+ <SingleSelect label="Coffee" isComboBox description="Avatar and inline">
96
+ {singleMockItems.map((item) => (
97
+ <SingleSelect.Item key={item.key} textValue={item.label} className="flex items-center gap-3">
98
+ <Avatar fullName="Senior Popsicle" size="small" />
99
+
100
+ <Text variant="body" className="flex-shrink-0 whitespace-nowrap mr-2">
101
+ {item.label}
102
+ </Text>
103
+
104
+ <Text variant="body" className="truncate">
105
+ Supporting text
106
+ </Text>
107
+ </SingleSelect.Item>
108
+ ))}
109
+ </SingleSelect>
110
+ ```
111
+
28
112
  ## Positioning and z-index Management
29
113
 
30
114
  The SingleSelect component leverages the native Popover API to manage its dropdown functionality. By using popover instead of custom portal logic, the component takes full advantage of CSS layers, ensuring dropdowns appear above other content without manual z-index management.
115
+
116
+ ## Known issues
117
+
118
+ ### Accessibility
119
+
120
+ Using the default SingleSelect component without combobox, there is a known accessibility issue for keyboard navigation when the component is used as the last focusable element on the page. After the list is opened pressing Tab should close the list, instead Tab brings the user to a unqiue state where they cannot use the escape key to close the list. Instead the user will have to navigate either forward of back again using the Tab key.
@@ -17,16 +17,9 @@ export default meta
17
17
  type Story = StoryObj<typeof meta>
18
18
 
19
19
  const args = {
20
+ label: 'Choose a coffee',
20
21
  items: singleMockItems,
21
- children: (
22
- <SingleSelect.List>
23
- {singleMockItems.map((item) => (
24
- <SingleSelect.ListItem key={item.value} id={item.value}>
25
- {item.label}
26
- </SingleSelect.ListItem>
27
- ))}
28
- </SingleSelect.List>
29
- ),
22
+ children: (item: any) => <SingleSelect.Item key={item.key}>{item.label}</SingleSelect.Item>,
30
23
  }
31
24
 
32
25
  export const RendersButton: Story = {
@@ -60,21 +53,6 @@ export const ClosesPopoverOnSelect: Story = {
60
53
  },
61
54
  }
62
55
 
63
- export const KeyboardNavigation: Story = {
64
- args,
65
- play: async () => {
66
- const trigger = screen.getByRole('button')
67
- trigger.focus()
68
- await userEvent.keyboard('{Enter}')
69
- await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'true'))
70
- const options = await screen.findAllByRole('option')
71
- await userEvent.keyboard('{ArrowDown}')
72
- expect(options[1]).toHaveAttribute('data-focused', 'true')
73
- await userEvent.keyboard('{ArrowUp}')
74
- expect(options[0]).toHaveAttribute('data-focused', 'true')
75
- },
76
- }
77
-
78
56
  export const KeyboardSelectsItem: Story = {
79
57
  args,
80
58
  play: async () => {
@@ -98,3 +76,23 @@ export const KeyboardEscapeClosesPopover: Story = {
98
76
  await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'false'))
99
77
  },
100
78
  }
79
+
80
+ export const XButtonClearsSelection: Story = {
81
+ args: { ...args, isComboBox: true },
82
+ play: async () => {
83
+ const input = screen.getByRole('combobox')
84
+
85
+ await userEvent.type(input, 'short')
86
+ const options = await screen.findAllByRole('option')
87
+ await userEvent.click(options[0])
88
+
89
+ const clearButton = await screen.findByRole('button', {
90
+ name: 'Clear Choose a coffee selection',
91
+ })
92
+ await waitFor(() => expect(clearButton).toBeVisible())
93
+ await userEvent.click(clearButton)
94
+ await waitFor(() => {
95
+ expect(input).toHaveValue('')
96
+ })
97
+ },
98
+ }