@stack-spot/citric-react 0.36.0 → 0.37.1-beta.1

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 (186) hide show
  1. package/dist/citric.css +2844 -2832
  2. package/dist/components/Accordion.d.ts +1 -1
  3. package/dist/components/Accordion.js +1 -1
  4. package/dist/components/Alert.d.ts +1 -1
  5. package/dist/components/Alert.js +1 -1
  6. package/dist/components/AsyncContent.d.ts +1 -1
  7. package/dist/components/AsyncContent.js +1 -1
  8. package/dist/components/Avatar.d.ts +1 -1
  9. package/dist/components/Avatar.js +1 -1
  10. package/dist/components/AvatarGroup.d.ts +1 -1
  11. package/dist/components/AvatarGroup.js +1 -1
  12. package/dist/components/Badge.d.ts +1 -1
  13. package/dist/components/Badge.js +1 -1
  14. package/dist/components/Blockquote.d.ts +1 -1
  15. package/dist/components/Blockquote.js +1 -1
  16. package/dist/components/Breadcrumb.d.ts +1 -1
  17. package/dist/components/Breadcrumb.js +1 -1
  18. package/dist/components/Button.d.ts +1 -1
  19. package/dist/components/Button.js +1 -1
  20. package/dist/components/ButtonLink.d.ts +1 -1
  21. package/dist/components/ButtonLink.js +1 -1
  22. package/dist/components/Card.d.ts +1 -1
  23. package/dist/components/Card.js +1 -1
  24. package/dist/components/Checkbox.d.ts +1 -1
  25. package/dist/components/Checkbox.js +1 -1
  26. package/dist/components/CheckboxGroup.d.ts +1 -1
  27. package/dist/components/CheckboxGroup.js +1 -1
  28. package/dist/components/Circle.d.ts +1 -1
  29. package/dist/components/Circle.js +1 -1
  30. package/dist/components/Divider.d.ts +1 -1
  31. package/dist/components/Divider.js +1 -1
  32. package/dist/components/ErrorBoundary.d.ts +1 -1
  33. package/dist/components/ErrorBoundary.js +1 -1
  34. package/dist/components/ErrorMessage.d.ts +1 -1
  35. package/dist/components/ErrorMessage.js +1 -1
  36. package/dist/components/FallbackBoundary.d.ts +1 -1
  37. package/dist/components/FallbackBoundary.js +1 -1
  38. package/dist/components/Favorite.d.ts +1 -1
  39. package/dist/components/Favorite.js +1 -1
  40. package/dist/components/FieldGroup.d.ts +1 -1
  41. package/dist/components/FieldGroup.js +1 -1
  42. package/dist/components/Form.d.ts +2 -2
  43. package/dist/components/Form.js +1 -1
  44. package/dist/components/FormGroup.d.ts +1 -1
  45. package/dist/components/FormGroup.js +1 -1
  46. package/dist/components/Icon.d.ts +1 -1
  47. package/dist/components/Icon.js +1 -1
  48. package/dist/components/IconBox.d.ts +3 -3
  49. package/dist/components/IconBox.js +1 -1
  50. package/dist/components/ImageBox.d.ts +3 -3
  51. package/dist/components/ImageBox.js +1 -1
  52. package/dist/components/ImageWithFallback.d.ts +1 -1
  53. package/dist/components/ImageWithFallback.js +1 -1
  54. package/dist/components/Input.d.ts +1 -1
  55. package/dist/components/Input.js +1 -1
  56. package/dist/components/Link.d.ts +1 -1
  57. package/dist/components/Link.js +1 -1
  58. package/dist/components/LoadingPanel.d.ts +1 -1
  59. package/dist/components/LoadingPanel.js +1 -1
  60. package/dist/components/MenuOverlay/Menu.d.ts +1 -1
  61. package/dist/components/MenuOverlay/Menu.js +1 -1
  62. package/dist/components/MenuOverlay/index.d.ts +1 -1
  63. package/dist/components/MenuOverlay/index.js +1 -1
  64. package/dist/components/Overlay/index.d.ts +1 -1
  65. package/dist/components/Overlay/index.js +1 -1
  66. package/dist/components/Pagination.d.ts +1 -1
  67. package/dist/components/Pagination.js +1 -1
  68. package/dist/components/ProgressBar.d.ts +1 -1
  69. package/dist/components/ProgressBar.js +1 -1
  70. package/dist/components/ProgressCircular.d.ts +1 -1
  71. package/dist/components/ProgressCircular.js +1 -1
  72. package/dist/components/RadioGroup.d.ts +1 -1
  73. package/dist/components/RadioGroup.js +1 -1
  74. package/dist/components/Rating.d.ts +17 -3
  75. package/dist/components/Rating.d.ts.map +1 -1
  76. package/dist/components/Rating.js +11 -3
  77. package/dist/components/Rating.js.map +1 -1
  78. package/dist/components/Select/MultiSelect.d.ts +1 -1
  79. package/dist/components/Select/MultiSelect.js +1 -1
  80. package/dist/components/Select/RichSelect.d.ts +1 -1
  81. package/dist/components/Select/RichSelect.js +1 -1
  82. package/dist/components/Select/SimpleSelect.d.ts +1 -1
  83. package/dist/components/Select/SimpleSelect.js +1 -1
  84. package/dist/components/Select/index.d.ts +1 -1
  85. package/dist/components/Select/index.js +1 -1
  86. package/dist/components/SelectBox.d.ts +1 -1
  87. package/dist/components/SelectBox.js +1 -1
  88. package/dist/components/Skeleton.d.ts +1 -1
  89. package/dist/components/Skeleton.js +1 -1
  90. package/dist/components/Slider.d.ts +1 -1
  91. package/dist/components/Slider.js +1 -1
  92. package/dist/components/SmartTable.d.ts +1 -1
  93. package/dist/components/SmartTable.js +1 -1
  94. package/dist/components/Stepper.d.ts +1 -1
  95. package/dist/components/Stepper.js +1 -1
  96. package/dist/components/Table.d.ts +3 -3
  97. package/dist/components/Table.js +1 -1
  98. package/dist/components/Tabs/index.d.ts +1 -1
  99. package/dist/components/Tabs/index.js +1 -1
  100. package/dist/components/Textarea.d.ts +1 -1
  101. package/dist/components/Textarea.js +1 -1
  102. package/dist/components/Tooltip.d.ts +1 -1
  103. package/dist/components/Tooltip.js +1 -1
  104. package/dist/context/CitricProvider.d.ts +1 -1
  105. package/dist/context/CitricProvider.js +1 -1
  106. package/dist/overlay.js +1 -1
  107. package/dist/theme.css +415 -415
  108. package/package.json +2 -2
  109. package/scripts/build-css.ts +49 -49
  110. package/src/components/Accordion.tsx +130 -130
  111. package/src/components/Alert.tsx +24 -24
  112. package/src/components/AsyncContent.tsx +70 -70
  113. package/src/components/Avatar.tsx +45 -45
  114. package/src/components/AvatarGroup.tsx +49 -49
  115. package/src/components/Badge.tsx +47 -47
  116. package/src/components/Blockquote.tsx +18 -18
  117. package/src/components/Breadcrumb.tsx +33 -33
  118. package/src/components/Button.tsx +105 -105
  119. package/src/components/ButtonLink.tsx +45 -45
  120. package/src/components/Card.tsx +68 -68
  121. package/src/components/Checkbox.tsx +51 -51
  122. package/src/components/CheckboxGroup.tsx +152 -152
  123. package/src/components/Circle.tsx +43 -43
  124. package/src/components/CitricComponent.ts +47 -47
  125. package/src/components/Divider.tsx +24 -24
  126. package/src/components/ErrorBoundary.tsx +75 -75
  127. package/src/components/ErrorMessage.tsx +11 -11
  128. package/src/components/FallbackBoundary.tsx +40 -40
  129. package/src/components/Favorite.tsx +57 -57
  130. package/src/components/FieldGroup.tsx +46 -46
  131. package/src/components/Form.tsx +36 -36
  132. package/src/components/FormGroup.tsx +57 -57
  133. package/src/components/Icon.tsx +35 -35
  134. package/src/components/IconBox.tsx +134 -134
  135. package/src/components/ImageBox.tsx +125 -125
  136. package/src/components/ImageWithFallback.tsx +65 -65
  137. package/src/components/Input.tsx +49 -49
  138. package/src/components/Link.tsx +55 -55
  139. package/src/components/LoadingPanel.tsx +8 -8
  140. package/src/components/MenuOverlay/Menu.tsx +158 -158
  141. package/src/components/MenuOverlay/context.ts +20 -20
  142. package/src/components/MenuOverlay/index.tsx +55 -55
  143. package/src/components/MenuOverlay/keyboard.ts +60 -60
  144. package/src/components/MenuOverlay/types.ts +171 -171
  145. package/src/components/Overlay/context.ts +10 -10
  146. package/src/components/Overlay/index.tsx +164 -164
  147. package/src/components/Overlay/types.ts +70 -70
  148. package/src/components/Pagination.tsx +113 -113
  149. package/src/components/ProgressBar.tsx +45 -45
  150. package/src/components/ProgressCircular.tsx +45 -45
  151. package/src/components/RadioGroup.tsx +146 -146
  152. package/src/components/Rating.tsx +98 -35
  153. package/src/components/Select/MultiSelect.tsx +346 -217
  154. package/src/components/Select/RichSelect.tsx +128 -128
  155. package/src/components/Select/SimpleSelect.tsx +73 -73
  156. package/src/components/Select/hooks.ts +133 -133
  157. package/src/components/Select/index.tsx +35 -35
  158. package/src/components/Select/types.ts +134 -134
  159. package/src/components/SelectBox.tsx +167 -167
  160. package/src/components/Skeleton.tsx +53 -53
  161. package/src/components/Slider.tsx +89 -89
  162. package/src/components/SmartTable.tsx +227 -227
  163. package/src/components/Stepper.tsx +163 -163
  164. package/src/components/Table.tsx +234 -234
  165. package/src/components/Tabs/TabController.ts +54 -54
  166. package/src/components/Tabs/index.tsx +87 -87
  167. package/src/components/Tabs/types.ts +54 -54
  168. package/src/components/Tabs/utils.ts +6 -6
  169. package/src/components/Text.ts +111 -111
  170. package/src/components/Textarea.tsx +27 -27
  171. package/src/components/Tooltip.tsx +72 -72
  172. package/src/components/layout.tsx +101 -101
  173. package/src/context/CitricContext.tsx +4 -4
  174. package/src/context/CitricProvider.tsx +14 -14
  175. package/src/context/hooks.ts +6 -6
  176. package/src/index.ts +58 -58
  177. package/src/overlay.ts +341 -341
  178. package/src/types.ts +216 -216
  179. package/src/utils/ValueController.ts +28 -28
  180. package/src/utils/acessibility.ts +92 -92
  181. package/src/utils/checkbox.ts +121 -121
  182. package/src/utils/css.ts +119 -119
  183. package/src/utils/options.ts +9 -9
  184. package/src/utils/radio.ts +93 -93
  185. package/src/utils/react.ts +6 -6
  186. package/tsconfig.json +10 -10
@@ -1,217 +1,346 @@
1
- import { listToClass } from '@stack-spot/portal-theme'
2
- import { useTranslate } from '@stack-spot/portal-translate'
3
- import { useEffect, useMemo, useRef, useState } from 'react'
4
- import { useCheckboxGroupControls } from '../../utils/checkbox'
5
- import { applyCSSVariable } from '../../utils/css'
6
- import { defaultRenderKey, defaultRenderLabel } from '../../utils/options'
7
- import { withRef } from '../../utils/react'
8
- import { Checkbox } from '../Checkbox'
9
- import { CheckboxGroup } from '../CheckboxGroup'
10
- import { CitricComponent } from '../CitricComponent'
11
- import { Input } from '../Input'
12
- import { Row } from '../layout'
13
- import { ProgressCircular } from '../ProgressCircular'
14
- import { useDisabledEffect, useFocusEffect, useOpenPanelEffect } from './hooks'
15
- import { RichSelectProps } from './types'
16
-
17
- export interface BaseMultiSelectProps<T> extends
18
- Omit<RichSelectProps<T>, 'value' | 'onChange' | 'renderHeader' | 'renderLabel' | 'renderOption' | 'required' | 'onFocus' | 'onBlur'> {
19
- value: T[],
20
- onChange: (value: T[]) => void,
21
- /**
22
- * A function to render the option in the selectable list.
23
- *
24
- * The `renderLabel` function is used if this is not provided.
25
- * @param value the option.
26
- * @returns the React Node.
27
- */
28
- renderOption?: (option: T) => React.ReactNode,
29
- /**
30
- * A function to render the selected options in the header.
31
- *
32
- * The `renderOption` function is used if this is not provided.
33
- * @param value the option.
34
- * @returns the React Node.
35
- */
36
- renderHeader?: (value: T[]) => React.ReactNode,
37
- /**
38
- * A function to render the item label.
39
- * @example
40
- * `(option) => option.name`
41
- * @default "the item's toString() result."
42
- * @param option the item to render.
43
- * @returns a React Node to render.
44
- */
45
- renderLabel?: (option: T) => string,
46
- /**
47
- * Whether or not to show a checkbox to select all or remove the selection.
48
- *
49
- * @default false
50
- */
51
- showSelectAll?: boolean,
52
- }
53
-
54
- export type MultiSelectProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'ref' | 'onChange' | 'onFocus' | 'onBlur'> &
55
- BaseMultiSelectProps<T>
56
-
57
- /**
58
- * A component that looks like a Select and behaves like a CheckboxGroup. This is a component that lets the user select multiple options
59
- * in a list.
60
- *
61
- * Differently than then the component Select, this does not render the native select of the browser. Instead, it renders a series of
62
- * checkboxes.
63
- *
64
- * @example
65
- *
66
- * ```
67
- * const options = useMemo(() => [
68
- * { id: 1, name: 'Option 1' },
69
- * { id: 2, name: 'Option 2' },
70
- * { id: 3, name: 'Option 3' },
71
- * ], [])
72
- *
73
- * const [value, setValue] = useState<typeof options>([])
74
- *
75
- * return <MultiSelect options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} setValue={setValue} />
76
- * ```
77
- */
78
- export const MultiSelect = withRef(
79
- function MultiSelect<T>({
80
- ref,
81
- options,
82
- value = [],
83
- onChange,
84
- renderLabel = defaultRenderLabel,
85
- renderKey = defaultRenderKey,
86
- disabled,
87
- loading,
88
- renderOption,
89
- renderHeader,
90
- searchable,
91
- maxHeight,
92
- style,
93
- className,
94
- showArrow,
95
- placeholder,
96
- showSelectAll,
97
- ...props
98
- }: MultiSelectProps<T>,
99
- ) {
100
- const t = useTranslate(dictionary)
101
- const _element = useRef<HTMLDivElement | null>(null)
102
- const element = ref ?? _element
103
- const [open, setOpen] = useState(false)
104
- const [focused, setFocused] = useState(false)
105
- const controls = useCheckboxGroupControls({
106
- options,
107
- renderKey,
108
- initialValue: value,
109
- onChange,
110
- applyFilter: (filter, option) => renderLabel(option).toLocaleLowerCase().includes(filter.toLocaleLowerCase()),
111
- })
112
-
113
- useOpenPanelEffect({ open, setOpen, setSearch: controls.setFilter, element, searchable })
114
- useFocusEffect({ element, focused, setFocused, setOpen })
115
- useDisabledEffect({ disabled, setOpen, setFocused })
116
-
117
- useEffect(() => {
118
- if (value !== controls.value) controls.setValue(value)
119
- }, [value.map(renderKey).join(',')])
120
-
121
- const header = useMemo(() => {
122
- if (value.length === 0) return <span className="placeholder header-text">{placeholder}</span>
123
- const reversed = [...value].reverse()
124
- return (
125
- (renderHeader?.(reversed)
126
- ?? (renderOption
127
- ? <Row className="header-text">{reversed.map(renderOption)}</Row>
128
- : <span className="header-text">{reversed.map(renderLabel).join(', ')}</span>
129
- )
130
- ) || <span></span>
131
- )}, [value, placeholder])
132
-
133
- return (
134
- <CitricComponent
135
- tag="div"
136
- component="multi-select"
137
- style={maxHeight ? applyCSSVariable(style, 'max-height', `${maxHeight}px`) : style}
138
- className={listToClass([
139
- className,
140
- showArrow === false && 'hide-arrow',
141
- open && 'open',
142
- focused && 'focused',
143
- disabled && 'disabled',
144
- ])}
145
- ref={element}
146
- aria-busy={loading}
147
- {...props}
148
- >
149
- <header
150
- onClick={() => {
151
- if (disabled) return
152
- setFocused(true)
153
- setOpen(true)
154
- }}
155
- onFocus={() => setFocused(true)}
156
- aria-label={t.accessibilityHelp}
157
- tabIndex={disabled ? undefined : 0}
158
- className={renderHeader ? 'custom' : undefined}
159
- >
160
- {header}
161
- {loading && <ProgressCircular size="xs" className="loader" />}
162
- </header>
163
- <div className="selection-panel" aria-hidden={!open} {...(open ? {} : { inert: 'true' })}>
164
- {searchable && <div className="search-bar">
165
- <div data-citric="field-group" className="auto">
166
- <i data-citric="icon-box" className="citric-icon outline Search"></i>
167
- <Input type="search" value={controls.filter} onChange={controls.setFilter} aria-label={t.searchAccessibility} />
168
- </div>
169
- </div>}
170
- {showSelectAll && (
171
- <Checkbox
172
- className="select-all"
173
- onChange={checked => checked ? controls.selectAll() : controls.removeSelection()}
174
- value={controls.isAllSelected}
175
- >
176
- {controls.isAllSelected ? t.removeSelection : t.selectAll}
177
- </Checkbox>
178
- )}
179
- <CheckboxGroup
180
- className="options"
181
- gap="0"
182
- options={controls.options}
183
- onChange={controls.setValue}
184
- value={controls.value}
185
- renderKey={controls.renderKey}
186
- focusable={false}
187
- renderItem={(checkbox, option) => (
188
- <CitricComponent
189
- component="checkbox-row"
190
- tag="label"
191
- className={listToClass(['option', controls.isUnfilteredButChecked(option) && 'unfiltered'])}
192
- >
193
- {checkbox}
194
- {renderOption?.(option) ?? renderLabel(option)}
195
- </CitricComponent>
196
- )}
197
- />
198
- </div>
199
- </CitricComponent>
200
- )
201
- },
202
- )
203
-
204
- const dictionary = {
205
- en: {
206
- accessibilityHelp: 'Press the arrow down to select multiple options',
207
- searchAccessibility: 'Filter the options',
208
- removeSelection: 'Remove selection',
209
- selectAll: 'Select all',
210
- },
211
- pt: {
212
- accessibilityHelp: 'Pressione a seta para baixo para selecionar múltiplas opções',
213
- searchAccessibility: 'Filtre as opções',
214
- removeSelection: 'Remover seleção',
215
- selectAll: 'Selecionar todos',
216
- },
217
- }
1
+ import { listToClass } from '@stack-spot/portal-theme'
2
+ import { useTranslate } from '@stack-spot/portal-translate'
3
+ import { useEffect, useMemo, useRef, useState } from 'react'
4
+ import { useCheckboxGroupControls } from '../../utils/checkbox'
5
+ import { applyCSSVariable } from '../../utils/css'
6
+ import { defaultRenderKey, defaultRenderLabel } from '../../utils/options'
7
+ import { withRef } from '../../utils/react'
8
+ import { Checkbox } from '../Checkbox'
9
+ import { CheckboxGroup } from '../CheckboxGroup'
10
+ import { CitricComponent } from '../CitricComponent'
11
+ import { IconButton } from '../IconBox'
12
+ import { Input } from '../Input'
13
+ import { Row } from '../layout'
14
+ import { ProgressCircular } from '../ProgressCircular'
15
+ import { useDisabledEffect, useFocusEffect, useOpenPanelEffect } from './hooks'
16
+ import { RichSelectProps } from './types'
17
+
18
+ export interface BaseMultiSelectProps<T> extends
19
+ Omit<RichSelectProps<T>, 'value' | 'onChange' | 'renderHeader' | 'renderLabel' | 'renderOption' | 'required' | 'onFocus' | 'onBlur'> {
20
+ value: T[],
21
+ onChange: (value: T[]) => void,
22
+ /**
23
+ * A function to render the option in the selectable list.
24
+ *
25
+ * The `renderLabel` function is used if this is not provided.
26
+ * @param value the option.
27
+ * @returns the React Node.
28
+ */
29
+ renderOption?: (option: T) => React.ReactNode,
30
+ /**
31
+ * A function to render the selected options in the header.
32
+ *
33
+ * The `renderOption` function is used if this is not provided.
34
+ * @param value the option.
35
+ * @returns the React Node.
36
+ */
37
+ renderHeader?: (value: T[]) => React.ReactNode,
38
+ /**
39
+ * A function to render the item label.
40
+ * @example
41
+ * `(option) => option.name`
42
+ * @default "the item's toString() result."
43
+ * @param option the item to render.
44
+ * @returns a React Node to render.
45
+ */
46
+ renderLabel?: (option: T) => string,
47
+ /**
48
+ * Whether or not to show a checkbox to select all or remove the selection.
49
+ *
50
+ * @default false
51
+ */
52
+ showSelectAll?: boolean,
53
+ /**
54
+ * Whether to render selected values as removable chips/tags.
55
+ *
56
+ * @default false
57
+ */
58
+ showAsChips?: boolean,
59
+ /**
60
+ * Whether to allow adding custom values that don't exist in options.
61
+ * When enabled, typing in the search and pressing Enter will add the value.
62
+ * The value will be added as a string to the list.
63
+ *
64
+ * @default false
65
+ */
66
+ allowCustomOptions?: boolean,
67
+ /**
68
+ * Function to create a new option from a string input.
69
+ * Required when `allowCustomOptions` is true.
70
+ *
71
+ * @param input the string input from the user
72
+ * @returns the new option of type T
73
+ */
74
+ createOption?: (inputValue: string) => T,
75
+ }
76
+
77
+ export type MultiSelectProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'ref' | 'onChange' | 'onFocus' | 'onBlur'> &
78
+ BaseMultiSelectProps<T>
79
+
80
+ /**
81
+ * A component that looks like a Select and behaves like a CheckboxGroup. This is a component that lets the user select multiple options
82
+ * in a list.
83
+ *
84
+ * Differently than then the component Select, this does not render the native select of the browser. Instead, it renders a series of
85
+ * checkboxes.
86
+ *
87
+ * @example
88
+ *
89
+ * ```
90
+ * const options = useMemo(() => [
91
+ * { id: 1, name: 'Option 1' },
92
+ * { id: 2, name: 'Option 2' },
93
+ * { id: 3, name: 'Option 3' },
94
+ * ], [])
95
+ *
96
+ * const [value, setValue] = useState<typeof options>([])
97
+ *
98
+ * return <MultiSelect options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} setValue={setValue} />
99
+ * ```
100
+ */
101
+ export const MultiSelect = withRef(
102
+ function MultiSelect<T>({
103
+ ref,
104
+ options,
105
+ value = [],
106
+ onChange,
107
+ renderLabel = defaultRenderLabel,
108
+ renderKey = defaultRenderKey,
109
+ disabled,
110
+ loading,
111
+ renderOption,
112
+ renderHeader,
113
+ searchable,
114
+ maxHeight,
115
+ style,
116
+ className,
117
+ showArrow,
118
+ placeholder,
119
+ showSelectAll,
120
+ showAsChips = false,
121
+ allowCustomOptions = false,
122
+ createOption,
123
+ ...props
124
+ }: MultiSelectProps<T>,
125
+ ) {
126
+ const t = useTranslate(dictionary)
127
+ const _element = useRef<HTMLDivElement | null>(null)
128
+ const element = ref ?? _element
129
+ const [open, setOpen] = useState(false)
130
+ const [focused, setFocused] = useState(false)
131
+
132
+ // Merge options with selected values that are not in the original options
133
+ const mergedOptions = useMemo(() => {
134
+ const optionKeys = new Set(options.map(renderKey))
135
+ const extraValues = value.filter(v => !optionKeys.has(renderKey(v)))
136
+ return [...options, ...extraValues]
137
+ }, [options, value, renderKey])
138
+
139
+ const controls = useCheckboxGroupControls({
140
+ options: mergedOptions,
141
+ renderKey,
142
+ initialValue: value,
143
+ onChange,
144
+ applyFilter: (filter, option) => {
145
+ const label = renderLabel(option)
146
+ if (!label) return false
147
+ return label.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
148
+ },
149
+ })
150
+
151
+ useOpenPanelEffect({ open, setOpen, setSearch: controls.setFilter, element, searchable })
152
+ useFocusEffect({ element, focused, setFocused, setOpen })
153
+ useDisabledEffect({ disabled, setOpen, setFocused })
154
+
155
+ useEffect(() => {
156
+ if (value !== controls.value) controls.setValue(value)
157
+ }, [value.map(renderKey).join(',')])
158
+
159
+ const handleRemoveChip = (option: T) => {
160
+ const newValue = value.filter(v => renderKey(v) !== renderKey(option))
161
+ controls.setValue(newValue)
162
+ }
163
+
164
+ const handleAddCustomValue = (e: React.KeyboardEvent<HTMLInputElement>) => {
165
+ if (!allowCustomOptions || !createOption || !controls.filter) return
166
+ const filterValue = String(controls.filter).trim()
167
+ if (e.key === 'Enter' && filterValue && filterValue.length > 0) {
168
+ e.preventDefault()
169
+ const newOption = createOption(filterValue)
170
+ const exists = value.some(v => {
171
+ const key1 = renderKey(v)
172
+ const key2 = renderKey(newOption)
173
+ if (typeof key1 === 'string' && typeof key2 === 'string') {
174
+ return key1.toLowerCase() === key2.toLowerCase()
175
+ }
176
+ return key1 === key2
177
+ })
178
+ if (!exists) {
179
+ controls.setValue([...value, newOption])
180
+ }
181
+ controls.setFilter('' as any)
182
+ }
183
+ }
184
+
185
+ // Check if the current filter does not match any existing option
186
+ const hasTemporaryOption = useMemo(() => {
187
+ if (!allowCustomOptions || !controls.filter) return false
188
+ const filterValue = String(controls.filter).trim()
189
+ if (!filterValue || filterValue.length === 0) return false
190
+
191
+ const matchesExisting = mergedOptions.some(option => {
192
+ const label = renderLabel(option)
193
+ if (!label) return false
194
+ return label.toLowerCase() === filterValue.toLowerCase()
195
+ })
196
+
197
+ return !matchesExisting
198
+ }, [allowCustomOptions, controls.filter, mergedOptions, renderLabel])
199
+
200
+ const header = useMemo(() => {
201
+ if (value.length === 0) return <span className="placeholder header-text">{placeholder}</span>
202
+ const reversed = [...value].reverse()
203
+
204
+ if (showAsChips) {
205
+ return (
206
+ <Row className="header-chips" gap="4px">
207
+ {reversed.map(option => (
208
+ <span
209
+ key={renderKey(option)}
210
+ data-citric="badge"
211
+ className="chip"
212
+ >
213
+ <span>{renderLabel(option)}</span>
214
+ {!disabled && (
215
+ <IconButton
216
+ icon="Times"
217
+ type="button"
218
+ className="remove-button"
219
+ size="xs"
220
+ disabled={disabled}
221
+ onClick={(e) => {
222
+ e.stopPropagation()
223
+ handleRemoveChip(option)
224
+ }}
225
+ aria-label={`${t.remove} ${renderLabel(option)}`}
226
+ />
227
+ )}
228
+ </span>
229
+ ))}
230
+ </Row>
231
+ )
232
+ }
233
+
234
+ return (
235
+ (renderHeader?.(reversed)
236
+ ?? (renderOption
237
+ ? <Row className="header-text">{reversed.map(renderOption)}</Row>
238
+ : <span className="header-text">{reversed.map(renderLabel).join(', ')}</span>
239
+ )
240
+ ) || <span></span>
241
+ )}, [value, placeholder, showAsChips, disabled])
242
+
243
+ return (
244
+ <CitricComponent
245
+ tag="div"
246
+ component="multi-select"
247
+ style={maxHeight ? applyCSSVariable(style, 'max-height', `${maxHeight}px`) : style}
248
+ className={listToClass([
249
+ className,
250
+ showArrow === false && 'hide-arrow',
251
+ open && 'open',
252
+ focused && 'focused',
253
+ disabled && 'disabled',
254
+ showAsChips && 'with-chips',
255
+ ])}
256
+ ref={element}
257
+ aria-busy={loading}
258
+ {...props}
259
+ >
260
+ <header
261
+ onClick={() => {
262
+ if (disabled) return
263
+ setFocused(true)
264
+ setOpen(true)
265
+ }}
266
+ onFocus={() => setFocused(true)}
267
+ aria-label={t.accessibilityHelp}
268
+ tabIndex={disabled ? undefined : 0}
269
+ className={renderHeader ? 'custom' : undefined}
270
+ >
271
+ {header}
272
+ {loading && <ProgressCircular size="xs" className="loader" />}
273
+ </header>
274
+ <div className="selection-panel" aria-hidden={!open} {...(open ? {} : { inert: 'true' })}>
275
+ {searchable && <div className="search-bar">
276
+ <div data-citric="field-group" className="auto">
277
+ <i data-citric="icon-box" className="citric-icon outline Search"></i>
278
+ <Input
279
+ type="search"
280
+ value={controls.filter}
281
+ onChange={controls.setFilter}
282
+ onKeyDown={handleAddCustomValue}
283
+ aria-label={t.searchAccessibility}
284
+ placeholder={allowCustomOptions ? t.searchOrAddPlaceholder : undefined}
285
+ />
286
+ </div>
287
+ </div>}
288
+ {showSelectAll && (
289
+ <Checkbox
290
+ className="select-all"
291
+ onChange={checked => checked ? controls.selectAll() : controls.removeSelection()}
292
+ value={controls.isAllSelected}
293
+ >
294
+ {controls.isAllSelected ? t.removeSelection : t.selectAll}
295
+ </Checkbox>
296
+ )}
297
+ <CheckboxGroup
298
+ className="options"
299
+ gap="0"
300
+ options={controls.options}
301
+ onChange={controls.setValue}
302
+ value={controls.value}
303
+ renderKey={controls.renderKey}
304
+ focusable={false}
305
+ renderItem={(checkbox, option) => (
306
+ <CitricComponent
307
+ component="checkbox-row"
308
+ tag="label"
309
+ className={listToClass(['option', controls.isUnfilteredButChecked(option) && 'unfiltered'])}
310
+ >
311
+ {checkbox}
312
+ {renderOption?.(option) ?? renderLabel(option)}
313
+ </CitricComponent>
314
+ )}
315
+ />
316
+ {hasTemporaryOption && (
317
+ <div className="temporary-option" style={{ fontStyle: 'italic', padding: '8px 16px', opacity: 0.7 }}>
318
+ {String(controls.filter).trim()} ({t.pressEnterToAdd})
319
+ </div>
320
+ )}
321
+ </div>
322
+ </CitricComponent>
323
+ )
324
+ },
325
+ )
326
+
327
+ const dictionary = {
328
+ en: {
329
+ accessibilityHelp: 'Press the arrow down to select multiple options',
330
+ searchAccessibility: 'Filter the options',
331
+ removeSelection: 'Remove selection',
332
+ selectAll: 'Select all',
333
+ remove: 'Remove',
334
+ searchOrAddPlaceholder: 'Search or press Enter to add',
335
+ pressEnterToAdd: 'press Enter to add',
336
+ },
337
+ pt: {
338
+ accessibilityHelp: 'Pressione a seta para baixo para selecionar múltiplas opções',
339
+ searchAccessibility: 'Filtre as opções',
340
+ removeSelection: 'Remover seleção',
341
+ selectAll: 'Selecionar todos',
342
+ remove: 'Remover',
343
+ searchOrAddPlaceholder: 'Busque ou pressione Enter para adicionar',
344
+ pressEnterToAdd: 'pressione Enter para adicionar',
345
+ },
346
+ }