@kaizen/components 2.0.0 → 2.0.2

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 (107) 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/Notification/InlineNotification/InlineNotification.cjs +1 -1
  5. package/dist/cjs/src/__alpha__/SingleSelect/SingleSelect.cjs +35 -74
  6. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.cjs +105 -0
  7. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css.cjs +11 -0
  8. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.cjs +112 -0
  9. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css.cjs +16 -0
  10. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/List/List.cjs +35 -10
  11. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.cjs +61 -8
  12. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css.cjs +10 -1
  13. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.cjs +38 -9
  14. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css.cjs +4 -1
  15. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.cjs +60 -30
  16. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.cjs +2 -1
  17. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.cjs +2 -1
  18. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.cjs +4 -2
  19. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Select/Select.cjs +87 -0
  20. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css.cjs +11 -0
  21. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.cjs +52 -0
  22. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css.cjs +13 -0
  23. package/dist/esm/alpha.mjs +1 -1
  24. package/dist/esm/src/Notification/InlineNotification/InlineNotification.mjs +1 -1
  25. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.mjs +39 -73
  26. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.mjs +96 -0
  27. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css.mjs +9 -0
  28. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.mjs +103 -0
  29. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css.mjs +14 -0
  30. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/List/List.mjs +37 -14
  31. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.mjs +63 -13
  32. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css.mjs +10 -1
  33. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.mjs +41 -15
  34. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css.mjs +4 -1
  35. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.mjs +69 -43
  36. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.mjs +2 -1
  37. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.mjs +2 -1
  38. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.mjs +4 -2
  39. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Select/Select.mjs +78 -0
  40. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css.mjs +9 -0
  41. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.mjs +43 -0
  42. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css.mjs +11 -0
  43. package/dist/styles.css +443 -79
  44. package/dist/types/__alpha__/SingleSelect/SingleSelect.d.ts +14 -19
  45. package/dist/types/__alpha__/SingleSelect/_docs/mockData.d.ts +3 -0
  46. package/dist/types/__alpha__/SingleSelect/context/SingleSelectContext.d.ts +15 -7
  47. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.d.ts +2 -0
  48. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBox/index.d.ts +1 -0
  49. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.d.ts +2 -0
  50. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/index.d.ts +1 -0
  51. package/dist/types/__alpha__/SingleSelect/subcomponents/List/List.d.ts +2 -7
  52. package/dist/types/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.d.ts +2 -7
  53. package/dist/types/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.d.ts +2 -9
  54. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/Popover.d.ts +3 -6
  55. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/index.d.ts +1 -0
  56. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.d.ts +1 -0
  57. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.d.ts +1 -0
  58. package/dist/types/__alpha__/SingleSelect/subcomponents/Select/Select.d.ts +2 -0
  59. package/dist/types/__alpha__/SingleSelect/subcomponents/Select/index.d.ts +1 -0
  60. package/dist/types/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.d.ts +2 -0
  61. package/dist/types/__alpha__/SingleSelect/subcomponents/SelectTrigger/index.d.ts +1 -0
  62. package/dist/types/__alpha__/SingleSelect/subcomponents/index.d.ts +4 -1
  63. package/dist/types/__alpha__/SingleSelect/types.d.ts +68 -11
  64. package/locales/en.json +9 -1
  65. package/package.json +9 -2
  66. package/src/Notification/InlineNotification/InlineNotification.tsx +1 -1
  67. package/src/__alpha__/SingleSelect/SingleSelect.tsx +35 -88
  68. package/src/__alpha__/SingleSelect/_docs/SingleSelect.mdx +96 -6
  69. package/src/__alpha__/SingleSelect/_docs/SingleSelect.spec.stories.tsx +22 -24
  70. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stickersheet.stories.tsx +389 -33
  71. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stories.tsx +41 -22
  72. package/src/__alpha__/SingleSelect/_docs/mockData.ts +20 -14
  73. package/src/__alpha__/SingleSelect/context/SingleSelectContext.tsx +18 -7
  74. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css +35 -0
  75. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.tsx +106 -0
  76. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/index.ts +1 -0
  77. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css +130 -0
  78. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.tsx +121 -0
  79. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/index.ts +1 -0
  80. package/src/__alpha__/SingleSelect/subcomponents/List/List.module.css +5 -0
  81. package/src/__alpha__/SingleSelect/subcomponents/List/List.tsx +36 -13
  82. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css +84 -3
  83. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.tsx +67 -11
  84. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css +20 -5
  85. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.tsx +46 -19
  86. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css +7 -5
  87. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.tsx +90 -37
  88. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/index.ts +1 -0
  89. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.ts +2 -2
  90. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.ts +9 -8
  91. package/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css +35 -0
  92. package/src/__alpha__/SingleSelect/subcomponents/Select/Select.tsx +84 -0
  93. package/src/__alpha__/SingleSelect/subcomponents/Select/index.ts +1 -0
  94. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css +77 -0
  95. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.tsx +52 -0
  96. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/index.ts +1 -0
  97. package/src/__alpha__/SingleSelect/subcomponents/index.ts +4 -1
  98. package/src/__alpha__/SingleSelect/types.ts +94 -14
  99. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.cjs +0 -57
  100. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css.cjs +0 -6
  101. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.mjs +0 -49
  102. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css.mjs +0 -4
  103. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.d.ts +0 -2
  104. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/index.d.ts +0 -1
  105. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css +0 -19
  106. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.tsx +0 -35
  107. package/src/__alpha__/SingleSelect/subcomponents/Trigger/index.ts +0 -1
@@ -1,6 +1,3 @@
1
- import React, { type PropsWithChildren } from 'react';
2
- import { type PopoverProps } from '../../types';
3
- export declare const Popover: {
4
- ({ buttonRef, popoverRef, racPopoverRef, children, }: PopoverProps & PropsWithChildren): React.ReactElement;
5
- displayName: string;
6
- };
1
+ import React from 'react';
2
+ import { type PopoverProps, type SelectItem } from '../../types';
3
+ export declare const Popover: <T extends SelectItem>({ state, popoverRef, children, clearButtonRef, ...restProps }: PopoverProps<T>) => React.ReactElement;
@@ -1,2 +1,3 @@
1
1
  export * from './usePopoverPositioning';
2
2
  export * from './useSupportsAnchorPositioning';
3
+ export * from './usePositioningStyles';
@@ -1,4 +1,5 @@
1
1
  import { type Position, type UsePopoverPositioningProps } from '../../../types';
2
2
  export declare function usePopoverPositioning({ triggerRef, popoverRef, direction, offset, preferredPlacement, }: UsePopoverPositioningProps): Position & {
3
3
  isPositioned: boolean;
4
+ updatePosition: () => void;
4
5
  };
@@ -1,4 +1,5 @@
1
1
  export declare const usePositioningStyles: (buttonRef: React.RefObject<HTMLElement>, popoverRef: React.RefObject<HTMLDivElement>, anchorName: string) => {
2
2
  popoverStyle: React.CSSProperties;
3
3
  isPositioned: boolean;
4
+ updatePosition: () => void;
4
5
  };
@@ -0,0 +1,2 @@
1
+ import { type SelectItem, type SelectProps } from '../../types';
2
+ export declare const Select: <T extends SelectItem>(props: SelectProps<T>) => JSX.Element;
@@ -0,0 +1 @@
1
+ export * from './Select';
@@ -0,0 +1,2 @@
1
+ import { type SelectTriggerProps } from '../../types';
2
+ export declare const SelectTrigger: ({ triggerProps, valueProps, buttonRef, }: SelectTriggerProps) => JSX.Element;
@@ -0,0 +1 @@
1
+ export * from './SelectTrigger';
@@ -1,5 +1,8 @@
1
1
  export * from './List';
2
2
  export * from './ListSection';
3
3
  export * from './ListItem';
4
- export * from './Trigger';
4
+ export * from './SelectTrigger';
5
+ export * from './ComboBoxTrigger';
5
6
  export * from './Popover';
7
+ export * from './Select';
8
+ export * from './ComboBox';
@@ -1,25 +1,66 @@
1
- import { type RefObject } from 'react';
2
- import { type Key } from '@react-types/shared';
1
+ import type { DOMAttributes, RefObject } from 'react';
2
+ import type React from 'react';
3
+ import { type ComboBoxState, type ComboBoxStateOptions } from '@react-stately/combobox';
4
+ import type { ListState } from '@react-stately/list';
5
+ import { type SelectState, type SelectStateOptions } from '@react-stately/select';
6
+ import { type Key, type Node } from '@react-types/shared';
7
+ import { type FocusableElement } from '@react-types/shared/src/dom';
8
+ import type { AriaButtonProps, AriaListBoxOptions, AriaPopoverProps } from 'react-aria';
3
9
  export type SelectItem = {
4
10
  label: string;
5
11
  value: string;
12
+ key: Key;
6
13
  };
7
14
  export type SelectSection = {
8
15
  label: string;
9
16
  options: SelectItem[];
10
17
  };
11
- export type SingleSelectProps = {
12
- children?: React.ReactNode;
13
- items: (SelectItem | SelectSection)[];
14
- onSelectionChange?: (key: Key | null) => void;
18
+ export type SelectLabel = {
19
+ labelHidden: true;
20
+ label: string;
21
+ } | {
22
+ labelHidden?: false;
23
+ label: React.ReactNode;
24
+ };
25
+ export type SelectBaseProps = {
26
+ variant?: 'primary' | 'secondary';
27
+ size?: 'small' | 'medium' | 'large';
28
+ labelPosition?: 'top' | 'side';
29
+ isReadOnly?: boolean;
30
+ } & SelectLabel;
31
+ export type SelectProps<T extends SelectItem> = Omit<SelectStateOptions<T>, 'label' | 'defaultFilter' | 'menuTrigger' | 'allowsCustomValue'> & SelectBaseProps;
32
+ export type ComboBoxProps<T extends SelectItem> = Omit<ComboBoxStateOptions<T>, 'label' | 'defaultFilter' | 'menuTrigger' | 'allowsCustomValue'> & SelectBaseProps;
33
+ export type SingleSelectProps<T extends SelectItem> = (ComboBoxProps<T> & {
34
+ isComboBox?: true;
35
+ }) | (SelectProps<T> & {
36
+ isComboBox?: false;
37
+ });
38
+ export type SelectTriggerProps = {
39
+ triggerProps: AriaButtonProps<'button'>;
40
+ valueProps: DOMAttributes<FocusableElement>;
41
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
42
+ };
43
+ export type ComboBoxTriggerProps = {
44
+ inputProps: React.InputHTMLAttributes<HTMLInputElement>;
45
+ inputRef: React.MutableRefObject<HTMLInputElement | null>;
46
+ buttonProps: AriaButtonProps<'button'>;
47
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
48
+ triggerWrapperRef: React.MutableRefObject<HTMLDivElement | null>;
49
+ clearButtonRef: React.MutableRefObject<HTMLButtonElement | null>;
50
+ };
51
+ export type ChevronButtonProps = AriaButtonProps<'button'> & {
52
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
15
53
  };
16
- export type TriggerProps = {
17
- buttonRef: React.RefObject<HTMLButtonElement>;
54
+ export type ClearButtonProps = {
55
+ clearButtonRef: React.RefObject<HTMLButtonElement>;
56
+ inputRef: React.RefObject<HTMLInputElement>;
18
57
  };
19
- export type PopoverProps = {
20
- buttonRef: React.RefObject<HTMLElement>;
58
+ export type PopoverProps<T extends SelectItem> = AriaPopoverProps & {
59
+ state: ComboBoxState<T> | SelectState<T>;
60
+ triggerRef: React.RefObject<HTMLElement>;
21
61
  popoverRef: React.RefObject<HTMLDivElement>;
22
- racPopoverRef: React.Ref<any>;
62
+ clearButtonRef?: React.RefObject<HTMLButtonElement>;
63
+ children: React.ReactNode;
23
64
  };
24
65
  type PositionDataProp = number | string | undefined;
25
66
  export type PositionData = {
@@ -42,4 +83,20 @@ export type UsePopoverPositioningProps = {
42
83
  offset?: number;
43
84
  preferredPlacement?: 'top' | 'bottom';
44
85
  };
86
+ export type ListProps<T extends SelectItem> = {
87
+ state: ComboBoxState<T> | SelectState<T>;
88
+ listBoxOptions: AriaListBoxOptions<T>;
89
+ listBoxRef: React.RefObject<HTMLUListElement>;
90
+ };
91
+ export type ListItemProps<T extends SelectItem> = {
92
+ item: Node<T>;
93
+ state: ListState<T>;
94
+ selectedIcon?: 'check' | 'radio';
95
+ selectedPosition?: 'start' | 'end';
96
+ className?: string;
97
+ };
98
+ export type ListSectionProps<T extends SelectItem> = {
99
+ section: Node<T>;
100
+ state: ComboBoxState<T> | SelectState<T>;
101
+ };
45
102
  export {};
package/locales/en.json CHANGED
@@ -185,8 +185,16 @@
185
185
  "description": "Prompts user to interact with button to hide information",
186
186
  "message": "Hide information:"
187
187
  },
188
+ "singleSelect.chevronButton": {
189
+ "description": "Aria label text for the SingleSelect button to open and close suggestions list",
190
+ "message": "Show suggestions for {field}"
191
+ },
192
+ "singleSelect.clearButtonAlt": {
193
+ "description": "Alt text for the clear selection button",
194
+ "message": "Clear {field} selection"
195
+ },
188
196
  "splitButton.dropdownButton.label": {
189
197
  "description": "Label for a dropdown menu holding additional actions",
190
198
  "message": "Additional actions"
191
199
  }
192
- }
200
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "bin",
17
17
  "codemods",
18
+ "alpha",
18
19
  "future",
19
20
  "next",
20
21
  "libs",
@@ -36,6 +37,11 @@
36
37
  "import": "./dist/esm/index.mjs",
37
38
  "require": "./dist/cjs/index.cjs"
38
39
  },
40
+ "./alpha": {
41
+ "types": "./dist/types/__alpha__/index.d.ts",
42
+ "import": "./dist/esm/alpha.mjs",
43
+ "require": "./dist/cjs/alpha.cjs"
44
+ },
39
45
  "./next": {
40
46
  "types": "./dist/types/__next__/index.d.ts",
41
47
  "import": "./dist/esm/next.mjs",
@@ -86,6 +92,7 @@
86
92
  "@react-aria/select": "^3.15.7",
87
93
  "@react-aria/utils": "^3.29.1",
88
94
  "@react-stately/collections": "^3.12.5",
95
+ "@react-stately/combobox": "^3.11.1",
89
96
  "@react-stately/datepicker": "^3.14.2",
90
97
  "@react-stately/list": "^3.12.3",
91
98
  "@react-stately/menu": "^3.9.5",
@@ -152,8 +159,8 @@
152
159
  "sass": "1.79.6",
153
160
  "serialize-query-params": "^2.0.2",
154
161
  "svgo": "^3.3.2",
155
- "tslib": "^2.8.1",
156
162
  "ts-patch": "^3.3.0",
163
+ "tslib": "^2.8.1",
157
164
  "tsx": "^4.20.3",
158
165
  "@kaizen/design-tokens": "11.0.0"
159
166
  },
@@ -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,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
+ }