@stack-spot/citric-react 0.41.2 → 0.42.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +13 -13
  2. package/dist/citric.css +3090 -2846
  3. package/dist/components/Accordion.d.ts +1 -1
  4. package/dist/components/Accordion.js +1 -1
  5. package/dist/components/Alert.d.ts +1 -1
  6. package/dist/components/Alert.js +1 -1
  7. package/dist/components/AsyncContent.d.ts +1 -1
  8. package/dist/components/AsyncContent.js +1 -1
  9. package/dist/components/Autocomplete/Autocomplete.d.ts +211 -0
  10. package/dist/components/Autocomplete/Autocomplete.d.ts.map +1 -0
  11. package/dist/components/Autocomplete/Autocomplete.js +409 -0
  12. package/dist/components/Autocomplete/Autocomplete.js.map +1 -0
  13. package/dist/components/Autocomplete/index.d.ts +3 -0
  14. package/dist/components/Autocomplete/index.d.ts.map +1 -0
  15. package/dist/components/Autocomplete/index.js +2 -0
  16. package/dist/components/Autocomplete/index.js.map +1 -0
  17. package/dist/components/Avatar.d.ts +1 -1
  18. package/dist/components/Avatar.js +1 -1
  19. package/dist/components/AvatarGroup.d.ts +1 -1
  20. package/dist/components/AvatarGroup.js +1 -1
  21. package/dist/components/Badge.d.ts +1 -1
  22. package/dist/components/Badge.js +1 -1
  23. package/dist/components/Blockquote.d.ts +1 -1
  24. package/dist/components/Blockquote.js +1 -1
  25. package/dist/components/Breadcrumb.d.ts +1 -1
  26. package/dist/components/Breadcrumb.js +1 -1
  27. package/dist/components/Button.d.ts +1 -1
  28. package/dist/components/Button.js +1 -1
  29. package/dist/components/ButtonLink.d.ts +1 -1
  30. package/dist/components/ButtonLink.js +1 -1
  31. package/dist/components/Card.d.ts +1 -1
  32. package/dist/components/Card.js +1 -1
  33. package/dist/components/Checkbox.d.ts +1 -1
  34. package/dist/components/Checkbox.d.ts.map +1 -1
  35. package/dist/components/Checkbox.js +2 -2
  36. package/dist/components/Checkbox.js.map +1 -1
  37. package/dist/components/CheckboxGroup.d.ts +1 -1
  38. package/dist/components/CheckboxGroup.js +1 -1
  39. package/dist/components/Circle.d.ts +1 -1
  40. package/dist/components/Circle.js +1 -1
  41. package/dist/components/CitricComponent.d.ts +1 -1
  42. package/dist/components/CitricComponent.d.ts.map +1 -1
  43. package/dist/components/Divider.d.ts +1 -1
  44. package/dist/components/Divider.js +1 -1
  45. package/dist/components/ErrorBoundary.d.ts +1 -1
  46. package/dist/components/ErrorBoundary.js +1 -1
  47. package/dist/components/ErrorMessage.d.ts +1 -1
  48. package/dist/components/ErrorMessage.js +1 -1
  49. package/dist/components/FallbackBoundary.d.ts +1 -1
  50. package/dist/components/FallbackBoundary.js +1 -1
  51. package/dist/components/Favorite.d.ts +1 -1
  52. package/dist/components/Favorite.js +1 -1
  53. package/dist/components/FieldGroup.d.ts +1 -1
  54. package/dist/components/FieldGroup.js +1 -1
  55. package/dist/components/Form.d.ts +2 -2
  56. package/dist/components/Form.js +1 -1
  57. package/dist/components/FormGroup.d.ts +1 -1
  58. package/dist/components/FormGroup.js +1 -1
  59. package/dist/components/Icon.d.ts +1 -1
  60. package/dist/components/Icon.js +1 -1
  61. package/dist/components/IconBox.d.ts +3 -3
  62. package/dist/components/IconBox.js +1 -1
  63. package/dist/components/ImageBox.d.ts +3 -3
  64. package/dist/components/ImageBox.js +1 -1
  65. package/dist/components/ImageWithFallback.d.ts +1 -1
  66. package/dist/components/ImageWithFallback.js +1 -1
  67. package/dist/components/Input.d.ts +1 -1
  68. package/dist/components/Input.js +1 -1
  69. package/dist/components/Link.d.ts +1 -1
  70. package/dist/components/Link.js +1 -1
  71. package/dist/components/LoadingPanel.d.ts +1 -1
  72. package/dist/components/LoadingPanel.js +1 -1
  73. package/dist/components/MenuOverlay/Menu.d.ts +1 -1
  74. package/dist/components/MenuOverlay/Menu.js +1 -1
  75. package/dist/components/MenuOverlay/index.d.ts +1 -1
  76. package/dist/components/MenuOverlay/index.js +1 -1
  77. package/dist/components/Overlay/index.d.ts +1 -1
  78. package/dist/components/Overlay/index.js +1 -1
  79. package/dist/components/Pagination.d.ts +1 -1
  80. package/dist/components/Pagination.js +1 -1
  81. package/dist/components/ProgressBar.d.ts +1 -1
  82. package/dist/components/ProgressBar.js +1 -1
  83. package/dist/components/ProgressCircular.d.ts +1 -1
  84. package/dist/components/ProgressCircular.js +1 -1
  85. package/dist/components/RadioGroup.d.ts +1 -1
  86. package/dist/components/RadioGroup.js +1 -1
  87. package/dist/components/Rating.d.ts +1 -1
  88. package/dist/components/Rating.js +1 -1
  89. package/dist/components/Select/MultiSelect.d.ts +1 -1
  90. package/dist/components/Select/MultiSelect.js +1 -1
  91. package/dist/components/Select/RichSelect.d.ts +1 -1
  92. package/dist/components/Select/RichSelect.js +1 -1
  93. package/dist/components/Select/SimpleSelect.d.ts +1 -1
  94. package/dist/components/Select/SimpleSelect.js +1 -1
  95. package/dist/components/Select/index.d.ts +1 -1
  96. package/dist/components/Select/index.js +1 -1
  97. package/dist/components/SelectBox.d.ts +1 -1
  98. package/dist/components/SelectBox.js +1 -1
  99. package/dist/components/Skeleton.d.ts +1 -1
  100. package/dist/components/Skeleton.js +1 -1
  101. package/dist/components/Slider.d.ts +1 -1
  102. package/dist/components/Slider.js +1 -1
  103. package/dist/components/SmartTable.d.ts +1 -1
  104. package/dist/components/SmartTable.js +1 -1
  105. package/dist/components/Stepper.d.ts +1 -1
  106. package/dist/components/Stepper.js +1 -1
  107. package/dist/components/Table.d.ts +3 -3
  108. package/dist/components/Table.js +1 -1
  109. package/dist/components/Tabs/index.d.ts +1 -1
  110. package/dist/components/Tabs/index.js +1 -1
  111. package/dist/components/Textarea.d.ts +1 -1
  112. package/dist/components/Textarea.js +1 -1
  113. package/dist/components/Tooltip.d.ts +1 -1
  114. package/dist/components/Tooltip.js +1 -1
  115. package/dist/context/CitricProvider.d.ts +1 -1
  116. package/dist/context/CitricProvider.js +1 -1
  117. package/dist/index.d.ts +1 -0
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/overlay.js +1 -1
  122. package/dist/theme.css +415 -415
  123. package/package.json +1 -1
  124. package/scripts/build-css.ts +49 -49
  125. package/src/components/Accordion.tsx +130 -130
  126. package/src/components/Alert.tsx +24 -24
  127. package/src/components/AsyncContent.tsx +75 -75
  128. package/src/components/Autocomplete/Autocomplete.tsx +794 -0
  129. package/src/components/Autocomplete/index.ts +3 -0
  130. package/src/components/Avatar.tsx +45 -45
  131. package/src/components/AvatarGroup.tsx +49 -49
  132. package/src/components/Badge.tsx +47 -47
  133. package/src/components/Blockquote.tsx +18 -18
  134. package/src/components/Breadcrumb.tsx +33 -33
  135. package/src/components/Button.tsx +105 -105
  136. package/src/components/ButtonLink.tsx +45 -45
  137. package/src/components/Card.tsx +68 -68
  138. package/src/components/Checkbox.tsx +52 -51
  139. package/src/components/CheckboxGroup.tsx +153 -153
  140. package/src/components/Circle.tsx +43 -43
  141. package/src/components/CitricComponent.ts +47 -47
  142. package/src/components/Divider.tsx +24 -24
  143. package/src/components/ErrorBoundary.tsx +75 -75
  144. package/src/components/ErrorMessage.tsx +11 -11
  145. package/src/components/FallbackBoundary.tsx +40 -40
  146. package/src/components/Favorite.tsx +57 -57
  147. package/src/components/FieldGroup.tsx +46 -46
  148. package/src/components/Form.tsx +36 -36
  149. package/src/components/FormGroup.tsx +57 -57
  150. package/src/components/Icon.tsx +35 -35
  151. package/src/components/IconBox.tsx +134 -134
  152. package/src/components/ImageBox.tsx +125 -125
  153. package/src/components/ImageWithFallback.tsx +65 -65
  154. package/src/components/Input.tsx +49 -49
  155. package/src/components/Link.tsx +55 -55
  156. package/src/components/LoadingPanel.tsx +12 -12
  157. package/src/components/MenuOverlay/Menu.tsx +158 -158
  158. package/src/components/MenuOverlay/context.ts +20 -20
  159. package/src/components/MenuOverlay/index.tsx +55 -55
  160. package/src/components/MenuOverlay/keyboard.ts +60 -60
  161. package/src/components/MenuOverlay/types.ts +171 -171
  162. package/src/components/Overlay/context.ts +10 -10
  163. package/src/components/Overlay/index.tsx +182 -182
  164. package/src/components/Overlay/types.ts +75 -75
  165. package/src/components/Pagination.tsx +133 -133
  166. package/src/components/ProgressBar.tsx +45 -45
  167. package/src/components/ProgressCircular.tsx +45 -45
  168. package/src/components/RadioGroup.tsx +147 -147
  169. package/src/components/Rating.tsx +98 -98
  170. package/src/components/Select/MultiSelect.tsx +217 -217
  171. package/src/components/Select/RichSelect.tsx +128 -128
  172. package/src/components/Select/SimpleSelect.tsx +73 -73
  173. package/src/components/Select/hooks.ts +133 -133
  174. package/src/components/Select/index.tsx +35 -35
  175. package/src/components/Select/types.ts +134 -134
  176. package/src/components/SelectBox.tsx +167 -167
  177. package/src/components/Skeleton.tsx +53 -53
  178. package/src/components/Slider.tsx +89 -89
  179. package/src/components/SmartTable.tsx +227 -227
  180. package/src/components/Stepper.tsx +163 -163
  181. package/src/components/Table.tsx +234 -234
  182. package/src/components/Tabs/TabController.ts +54 -54
  183. package/src/components/Tabs/index.tsx +106 -106
  184. package/src/components/Tabs/types.ts +67 -67
  185. package/src/components/Tabs/utils.ts +6 -6
  186. package/src/components/Text.ts +111 -111
  187. package/src/components/Textarea.tsx +27 -27
  188. package/src/components/Tooltip.tsx +83 -83
  189. package/src/components/layout.tsx +101 -101
  190. package/src/context/CitricContext.tsx +4 -4
  191. package/src/context/CitricProvider.tsx +14 -14
  192. package/src/context/hooks.ts +6 -6
  193. package/src/index.ts +59 -58
  194. package/src/overlay.ts +348 -348
  195. package/src/types.ts +235 -235
  196. package/src/utils/ValueController.ts +28 -28
  197. package/src/utils/acessibility.ts +92 -92
  198. package/src/utils/checkbox.ts +121 -121
  199. package/src/utils/css.ts +119 -119
  200. package/src/utils/options.ts +9 -9
  201. package/src/utils/radio.ts +93 -93
  202. package/src/utils/react.ts +6 -6
  203. package/src/utils/time.ts +5 -5
  204. package/tsconfig.json +10 -10
@@ -67,5 +67,5 @@ export type AccordionProps = Omit<React.JSX.IntrinsicElements['div'], 'onChange'
67
67
  * )
68
68
  * ```
69
69
  */
70
- export declare const Accordion: ({ id, appearance, expanded, onChange, header, maxHeight, startExpanded, className, style, children, ...props }: AccordionProps) => import("react/jsx-runtime").JSX.Element;
70
+ export declare const Accordion: ({ id, appearance, expanded, onChange, header, maxHeight, startExpanded, className, style, children, ...props }: AccordionProps) => import("react/jsx-runtime.js").JSX.Element;
71
71
  //# sourceMappingURL=Accordion.d.ts.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime.js";
2
2
  import { listToClass } from '@stack-spot/portal-theme';
3
3
  import { useTranslate } from '@stack-spot/portal-translate';
4
4
  import { useEffect, useMemo, useState } from 'react';
@@ -14,5 +14,5 @@ export type AlertProps = React.JSX.IntrinsicElements['div'] & BaseAlertProps;
14
14
  * <Alert type="warning">My warning!</Alert>
15
15
  * ```
16
16
  */
17
- export declare const Alert: ({ type, className, children, ...props }: AlertProps) => import("react/jsx-runtime").JSX.Element;
17
+ export declare const Alert: ({ type, className, children, ...props }: AlertProps) => import("react/jsx-runtime.js").JSX.Element;
18
18
  //# sourceMappingURL=Alert.d.ts.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime.js";
2
2
  import { listToClass } from '@stack-spot/portal-theme';
3
3
  import { withRef } from '../utils/react.js';
4
4
  import { CitricComponent } from './CitricComponent.js';
@@ -45,6 +45,6 @@ interface Props {
45
45
  * )
46
46
  * ```
47
47
  */
48
- export declare const AsyncContent: ({ loading, error, autofocus, children, loadingStyle }: Props) => string | number | boolean | Iterable<import("react").ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
48
+ export declare const AsyncContent: ({ loading, error, autofocus, children, loadingStyle }: Props) => string | number | boolean | Iterable<import("react").ReactNode> | import("react/jsx-runtime.js").JSX.Element | null | undefined;
49
49
  export {};
50
50
  //# sourceMappingURL=AsyncContent.d.ts.map
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime.js";
2
2
  import { useEffect, useLayoutEffect } from 'react';
3
3
  import { useCitricController } from '../context/hooks.js';
4
4
  import { withRef } from '../utils/react.js';
@@ -0,0 +1,211 @@
1
+ import { ColorPaletteName, ColorSchemeName } from '@stack-spot/portal-theme';
2
+ export interface BaseAutocompleteProps<T, Multiple extends boolean = false> {
3
+ /**
4
+ * The list of options available for selection.
5
+ */
6
+ options: T[];
7
+ /**
8
+ * The current value(s) selected.
9
+ * - Single selection: T | null
10
+ * - Multiple selection: T[]
11
+ */
12
+ value: Multiple extends true ? T[] : (T | null);
13
+ /**
14
+ * Callback fired when the value changes.
15
+ */
16
+ onChange: Multiple extends true ? (value: T[]) => void : (value: T | null) => void;
17
+ /**
18
+ * If true, enables multiple selection mode.
19
+ * @default false
20
+ */
21
+ multiple?: Multiple;
22
+ /**
23
+ * If true, allows the user to enter values that are not in the options list.
24
+ * @default false
25
+ */
26
+ freeSolo?: boolean;
27
+ /**
28
+ * If true, allows creating new options when no match is found.
29
+ * Shows an "Add [value]" option at the top of the list.
30
+ * @default false
31
+ */
32
+ creatable?: boolean;
33
+ /**
34
+ * Callback fired when a new option is created.
35
+ * Required when creatable is true and you want manual control.
36
+ */
37
+ onCreate?: (inputValue: string) => void;
38
+ /**
39
+ * Function to create a new option object from the input value.
40
+ * Required when creatable is true without onCreate and working with objects.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * getOptionFromInput={(inputValue) => ({ id: Date.now(), name: inputValue })}
45
+ * ```
46
+ */
47
+ getOptionFromInput?: (inputValue: string) => T;
48
+ /**
49
+ * The input value (controlled).
50
+ */
51
+ inputValue?: string;
52
+ /**
53
+ * Callback fired when the input value changes.
54
+ */
55
+ onInputChange?: (value: string) => void;
56
+ /**
57
+ * A function to render the item label.
58
+ * @default "the item's toString() result."
59
+ */
60
+ renderLabel?: (option: T) => string;
61
+ /**
62
+ * A function to generate a unique key for each option.
63
+ */
64
+ renderKey?: (option: T) => string | number;
65
+ /**
66
+ * A function to render an option in the dropdown.
67
+ */
68
+ renderOption?: (option: T) => React.ReactNode;
69
+ /**
70
+ * A function to render a tag in multiple mode.
71
+ */
72
+ renderTag?: (option: T, onRemove: () => void) => React.ReactNode;
73
+ /**
74
+ * Custom filter function for options.
75
+ * @default filters by label includes input (case insensitive)
76
+ */
77
+ filterOptions?: (options: T[], inputValue: string) => T[];
78
+ /**
79
+ * If true, shows a loading indicator.
80
+ * @default false
81
+ */
82
+ loading?: boolean;
83
+ /**
84
+ * If true, the component is disabled.
85
+ * @default false
86
+ */
87
+ disabled?: boolean;
88
+ /**
89
+ * Placeholder text for the input.
90
+ */
91
+ placeholder?: string;
92
+ /**
93
+ * Maximum height for the dropdown panel in pixels.
94
+ */
95
+ maxHeight?: number;
96
+ /**
97
+ * Maximum number of tags to show before truncating.
98
+ * Only applies when multiple is true.
99
+ */
100
+ maxTagsToShow?: number;
101
+ /**
102
+ * If true, automatically highlights the first option.
103
+ * @default false
104
+ */
105
+ autoHighlight?: boolean;
106
+ /**
107
+ * If true, clears the input value when an option is selected.
108
+ * Only applies when multiple is true.
109
+ * @default true (for multiple)
110
+ */
111
+ clearOnSelect?: boolean;
112
+ /**
113
+ * If true, the popup will open on input focus.
114
+ * @default true
115
+ */
116
+ openOnFocus?: boolean;
117
+ /**
118
+ * Callback fired when an option is selected (before onChange).
119
+ */
120
+ onSelect?: (option: T | null) => void;
121
+ /**
122
+ * Text to display when no options are available.
123
+ */
124
+ noOptionsText?: string;
125
+ /**
126
+ * Text to display when loading.
127
+ */
128
+ loadingText?: string;
129
+ /**
130
+ * Callback fired when the user scrolls to the end of the options list.
131
+ * Useful for implementing infinite scroll/pagination.
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * <Autocomplete
136
+ * options={options}
137
+ * onScrollEnd={() => fetchMoreOptions()}
138
+ * loading={isFetchingMore}
139
+ * />
140
+ * ```
141
+ */
142
+ onScrollEnd?: () => void;
143
+ /**
144
+ * Margin in pixels before the end of the list to trigger onScrollEnd.
145
+ * @default 200
146
+ */
147
+ scrollEndMargin?: number;
148
+ /**
149
+ * Color scheme for the tags (badges) in multiple mode.
150
+ * @example 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
151
+ */
152
+ tagColorScheme?: ColorSchemeName;
153
+ /**
154
+ * Color palette for the tags (badges) in multiple mode.
155
+ * @example 'blue' | 'green' | 'red' | 'yellow' | 'purple'
156
+ */
157
+ tagColorPalette?: ColorPaletteName;
158
+ /**
159
+ * Appearance of the tags (badges) in multiple mode.
160
+ * @default 'circle'
161
+ */
162
+ tagAppearance?: 'square' | 'circle';
163
+ }
164
+ export type AutocompleteProps<T, Multiple extends boolean = false> = Omit<React.JSX.IntrinsicElements['div'], 'ref' | 'onChange' | 'onSelect'> & BaseAutocompleteProps<T, Multiple>;
165
+ /**
166
+ * A combination of a text input and a dropdown that suggests options as the user types.
167
+ * Supports both single and multiple selection modes, similar to Material-UI Autocomplete.
168
+ *
169
+ * @example
170
+ * Basic usage (single selection):
171
+ * ```tsx
172
+ * const [value, setValue] = useState<Option | null>(null)
173
+ *
174
+ * <Autocomplete
175
+ * options={options}
176
+ * value={value}
177
+ * onChange={setValue}
178
+ * renderLabel={o => o.name}
179
+ * renderKey={o => o.id}
180
+ * />
181
+ * ```
182
+ *
183
+ * @example
184
+ * Multiple selection with tags:
185
+ * ```tsx
186
+ * const [value, setValue] = useState<Option[]>([])
187
+ *
188
+ * <Autocomplete
189
+ * multiple
190
+ * options={options}
191
+ * value={value}
192
+ * onChange={setValue}
193
+ * renderLabel={o => o.name}
194
+ * renderKey={o => o.id}
195
+ * />
196
+ * ```
197
+ *
198
+ * @example
199
+ * Free solo (allow custom values):
200
+ * ```tsx
201
+ * <Autocomplete
202
+ * freeSolo
203
+ * options={options}
204
+ * value={value}
205
+ * onChange={setValue}
206
+ * renderLabel={o => o.name}
207
+ * />
208
+ * ```
209
+ */
210
+ export declare const Autocomplete: <T, Multiple extends boolean = false>(props: AutocompleteProps<T, Multiple>) => React.ReactElement;
211
+ //# sourceMappingURL=Autocomplete.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../../src/components/Autocomplete/Autocomplete.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,409 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime.js";
2
+ import { listToClass } from '@stack-spot/portal-theme';
3
+ import { useTranslate } from '@stack-spot/portal-translate';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { applyCSSVariable } from '../../utils/css.js';
6
+ import { defaultRenderKey, defaultRenderLabel } from '../../utils/options.js';
7
+ import { withRef } from '../../utils/react.js';
8
+ import { Badge } from '../Badge.js';
9
+ import { Checkbox } from '../Checkbox.js';
10
+ import { CitricComponent } from '../CitricComponent.js';
11
+ import { IconButton } from '../IconBox.js';
12
+ import { ProgressCircular } from '../ProgressCircular.js';
13
+ import { useDisabledEffect, useFocusEffect } from '../Select/hooks.js';
14
+ import { Row } from '../layout.js';
15
+ /**
16
+ * A combination of a text input and a dropdown that suggests options as the user types.
17
+ * Supports both single and multiple selection modes, similar to Material-UI Autocomplete.
18
+ *
19
+ * @example
20
+ * Basic usage (single selection):
21
+ * ```tsx
22
+ * const [value, setValue] = useState<Option | null>(null)
23
+ *
24
+ * <Autocomplete
25
+ * options={options}
26
+ * value={value}
27
+ * onChange={setValue}
28
+ * renderLabel={o => o.name}
29
+ * renderKey={o => o.id}
30
+ * />
31
+ * ```
32
+ *
33
+ * @example
34
+ * Multiple selection with tags:
35
+ * ```tsx
36
+ * const [value, setValue] = useState<Option[]>([])
37
+ *
38
+ * <Autocomplete
39
+ * multiple
40
+ * options={options}
41
+ * value={value}
42
+ * onChange={setValue}
43
+ * renderLabel={o => o.name}
44
+ * renderKey={o => o.id}
45
+ * />
46
+ * ```
47
+ *
48
+ * @example
49
+ * Free solo (allow custom values):
50
+ * ```tsx
51
+ * <Autocomplete
52
+ * freeSolo
53
+ * options={options}
54
+ * value={value}
55
+ * onChange={setValue}
56
+ * renderLabel={o => o.name}
57
+ * />
58
+ * ```
59
+ */
60
+ export const Autocomplete = withRef(function Autocomplete({ options, value, onChange, multiple = false, freeSolo = false, creatable = false, onCreate, getOptionFromInput, inputValue: controlledInputValue, onInputChange, renderLabel = defaultRenderLabel, renderKey = defaultRenderKey, renderOption, renderTag, filterOptions, loading = false, disabled = false, placeholder, maxHeight, maxTagsToShow, autoHighlight = false, clearOnSelect = multiple, openOnFocus = true, onSelect, noOptionsText, loadingText, onScrollEnd, scrollEndMargin = 200, tagColorScheme, tagColorPalette, tagAppearance = 'circle', style, className, ...props }, ref) {
61
+ const t = useTranslate(dictionary);
62
+ const _element = useRef(null);
63
+ const inputRef = useRef(null);
64
+ const dropdownRef = useRef(null);
65
+ const element = ref ?? _element;
66
+ const [open, setOpen] = useState(false);
67
+ const [focused, setFocused] = useState(false);
68
+ const [internalInputValue, setInternalInputValue] = useState('');
69
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
70
+ useFocusEffect({ element, focused, setFocused, setOpen });
71
+ useDisabledEffect({ disabled, setOpen, setFocused });
72
+ useEffect(() => {
73
+ if (!open)
74
+ return;
75
+ const handleClickOutside = (event) => {
76
+ if (element.current && !element.current.contains(event.target)) {
77
+ setOpen(false);
78
+ setFocused(false);
79
+ }
80
+ };
81
+ setTimeout(() => {
82
+ document.addEventListener('click', handleClickOutside);
83
+ }, 10);
84
+ return () => {
85
+ document.removeEventListener('click', handleClickOutside);
86
+ };
87
+ }, [open, element]);
88
+ const inputValue = controlledInputValue ?? internalInputValue;
89
+ const setInputValue = useCallback((newValue) => {
90
+ if (onInputChange) {
91
+ onInputChange(newValue);
92
+ }
93
+ else {
94
+ setInternalInputValue(newValue);
95
+ }
96
+ }, [onInputChange]);
97
+ const defaultFilter = useCallback((opts, input) => {
98
+ if (!input)
99
+ return opts;
100
+ return opts.filter(option => renderLabel(option).toLowerCase().includes(input.toLowerCase()));
101
+ }, [renderLabel]);
102
+ const filter = filterOptions ?? defaultFilter;
103
+ const filteredOptions = useMemo(() => filter(options, inputValue), [options, inputValue, filter]);
104
+ const showCreateOption = useMemo(() => {
105
+ if (!creatable || !onCreate || !inputValue.trim())
106
+ return false;
107
+ const hasExactMatch = filteredOptions.some(option => renderLabel(option).toLowerCase() === inputValue.toLowerCase());
108
+ return !hasExactMatch;
109
+ }, [creatable, onCreate, inputValue, filteredOptions, renderLabel]);
110
+ const handleCreate = useCallback(() => {
111
+ if (!onCreate || !inputValue.trim())
112
+ return;
113
+ onCreate(inputValue.trim());
114
+ setInputValue('');
115
+ if (inputRef.current) {
116
+ inputRef.current.focus();
117
+ }
118
+ }, [onCreate, inputValue, setInputValue]);
119
+ const isSelected = useCallback((option) => {
120
+ if (multiple) {
121
+ return value.some(v => renderKey(v) === renderKey(option));
122
+ }
123
+ return value !== null && renderKey(value) === renderKey(option);
124
+ }, [value, multiple, renderKey]);
125
+ const handleSelect = useCallback((option) => {
126
+ if (onSelect)
127
+ onSelect(option);
128
+ if (multiple) {
129
+ const currentValue = value;
130
+ const isAlreadySelected = currentValue.some(v => renderKey(v) === renderKey(option));
131
+ if (isAlreadySelected) {
132
+ const newValue = currentValue.filter(v => renderKey(v) !== renderKey(option));
133
+ onChange(newValue);
134
+ }
135
+ else {
136
+ onChange([...currentValue, option]);
137
+ }
138
+ if (clearOnSelect) {
139
+ setInputValue('');
140
+ }
141
+ }
142
+ else {
143
+ onChange(option);
144
+ setInputValue(renderLabel(option));
145
+ setOpen(false);
146
+ }
147
+ }, [multiple, value, onChange, renderKey, renderLabel, clearOnSelect, setInputValue, onSelect]);
148
+ const handleRemoveTag = useCallback((optionToRemove) => {
149
+ if (!multiple)
150
+ return;
151
+ const newValue = value.filter(v => renderKey(v) !== renderKey(optionToRemove));
152
+ onChange(newValue);
153
+ }, [multiple, value, onChange, renderKey]);
154
+ const handleInputChange = (newValue) => {
155
+ setInputValue(newValue);
156
+ if (!open && newValue) {
157
+ setOpen(true);
158
+ }
159
+ setHighlightedIndex(autoHighlight ? 0 : -1);
160
+ };
161
+ const handleFocus = () => {
162
+ setFocused(true);
163
+ if (openOnFocus) {
164
+ setOpen(true);
165
+ }
166
+ };
167
+ const handleBlur = (e) => {
168
+ if (element.current?.contains(e.relatedTarget)) {
169
+ return;
170
+ }
171
+ setFocused(false);
172
+ setOpen(false);
173
+ if (freeSolo && inputValue && !multiple) {
174
+ if (creatable && !onCreate) {
175
+ if (getOptionFromInput) {
176
+ const newOption = getOptionFromInput(inputValue.trim());
177
+ onChange(newOption);
178
+ }
179
+ else {
180
+ onChange(inputValue);
181
+ }
182
+ }
183
+ else {
184
+ const exactMatch = options.find(o => renderLabel(o).toLowerCase() === inputValue.toLowerCase());
185
+ if (exactMatch) {
186
+ handleSelect(exactMatch);
187
+ }
188
+ }
189
+ }
190
+ };
191
+ const handleKeyDown = (e) => {
192
+ if (disabled)
193
+ return;
194
+ switch (e.key) {
195
+ case 'ArrowDown':
196
+ e.preventDefault();
197
+ if (!open) {
198
+ setOpen(true);
199
+ }
200
+ else {
201
+ setHighlightedIndex(prev => prev < filteredOptions.length - 1 ? prev + 1 : prev);
202
+ }
203
+ break;
204
+ case 'ArrowUp':
205
+ e.preventDefault();
206
+ if (open) {
207
+ setHighlightedIndex(prev => prev > 0 ? prev - 1 : 0);
208
+ }
209
+ break;
210
+ case 'Enter':
211
+ e.preventDefault();
212
+ if (open && highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
213
+ handleSelect(filteredOptions[highlightedIndex]);
214
+ }
215
+ else if (creatable && inputValue.trim()) {
216
+ if (onCreate) {
217
+ handleCreate();
218
+ }
219
+ else if (freeSolo && getOptionFromInput) {
220
+ const newOption = getOptionFromInput(inputValue.trim());
221
+ if (multiple) {
222
+ const currentValue = value;
223
+ const isDuplicate = currentValue.some(v => renderKey(v) === renderKey(newOption));
224
+ if (!isDuplicate) {
225
+ onChange([...currentValue, newOption]);
226
+ }
227
+ setInputValue('');
228
+ }
229
+ else {
230
+ onChange(newOption);
231
+ setInputValue(renderLabel(newOption));
232
+ setOpen(false);
233
+ }
234
+ }
235
+ else if (freeSolo) {
236
+ if (multiple) {
237
+ const currentValue = value;
238
+ const inputAsOption = inputValue;
239
+ const isDuplicate = currentValue.some(v => renderLabel(v).toLowerCase() === inputValue.toLowerCase());
240
+ if (!isDuplicate) {
241
+ onChange([...currentValue, inputAsOption]);
242
+ }
243
+ setInputValue('');
244
+ }
245
+ else {
246
+ onChange(inputValue);
247
+ setOpen(false);
248
+ }
249
+ }
250
+ }
251
+ else if (freeSolo && inputValue && !multiple) {
252
+ const exactMatch = options.find(o => renderLabel(o).toLowerCase() === inputValue.toLowerCase());
253
+ if (exactMatch) {
254
+ handleSelect(exactMatch);
255
+ }
256
+ }
257
+ break;
258
+ case 'Escape':
259
+ e.preventDefault();
260
+ setOpen(false);
261
+ if (inputRef.current) {
262
+ inputRef.current.blur();
263
+ }
264
+ break;
265
+ case 'Backspace':
266
+ if (multiple && !inputValue && value.length > 0) {
267
+ const lastTag = value[value.length - 1];
268
+ handleRemoveTag(lastTag);
269
+ }
270
+ break;
271
+ default:
272
+ break;
273
+ }
274
+ };
275
+ const handleClear = () => {
276
+ if (multiple) {
277
+ onChange([]);
278
+ }
279
+ else {
280
+ onChange(null);
281
+ }
282
+ setInputValue('');
283
+ if (inputRef.current) {
284
+ inputRef.current.focus();
285
+ }
286
+ };
287
+ useEffect(() => {
288
+ if (!multiple && value && !focused) {
289
+ setInternalInputValue(renderLabel(value));
290
+ }
291
+ }, [value, multiple, renderLabel, focused]);
292
+ useEffect(() => {
293
+ if (autoHighlight && open && filteredOptions.length > 0) {
294
+ setHighlightedIndex(0);
295
+ }
296
+ }, [autoHighlight, open, filteredOptions.length]);
297
+ useEffect(() => {
298
+ if (highlightedIndex < 0 || !open)
299
+ return;
300
+ const optionsContainer = dropdownRef.current?.querySelector('.options');
301
+ if (!optionsContainer)
302
+ return;
303
+ const highlightedOption = optionsContainer.children[highlightedIndex];
304
+ if (!highlightedOption)
305
+ return;
306
+ const containerRect = optionsContainer.getBoundingClientRect();
307
+ const optionRect = highlightedOption.getBoundingClientRect();
308
+ if (optionRect.bottom > containerRect.bottom) {
309
+ highlightedOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
310
+ }
311
+ else if (optionRect.top < containerRect.top) {
312
+ highlightedOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
313
+ }
314
+ }, [highlightedIndex, open]);
315
+ useEffect(() => {
316
+ if (!onScrollEnd || !open)
317
+ return;
318
+ const optionsContainer = dropdownRef.current?.querySelector('.options');
319
+ if (!optionsContainer)
320
+ return;
321
+ const handleScroll = () => {
322
+ if (loading)
323
+ return;
324
+ const { scrollTop, scrollHeight, clientHeight } = optionsContainer;
325
+ const scrollBottom = scrollHeight - scrollTop - clientHeight;
326
+ if (scrollBottom <= scrollEndMargin) {
327
+ onScrollEnd();
328
+ }
329
+ };
330
+ optionsContainer.addEventListener('scroll', handleScroll);
331
+ handleScroll();
332
+ return () => {
333
+ optionsContainer.removeEventListener('scroll', handleScroll);
334
+ };
335
+ }, [onScrollEnd, open, filteredOptions.length, loading, scrollEndMargin]);
336
+ const renderTags = () => {
337
+ if (!multiple || value.length === 0)
338
+ return null;
339
+ const tags = value;
340
+ const visibleTags = maxTagsToShow && tags.length > maxTagsToShow
341
+ ? tags.slice(0, maxTagsToShow)
342
+ : tags;
343
+ const remainingCount = maxTagsToShow && tags.length > maxTagsToShow
344
+ ? tags.length - maxTagsToShow
345
+ : 0;
346
+ return (_jsxs(_Fragment, { children: [visibleTags.map(tag => {
347
+ if (renderTag) {
348
+ return renderTag(tag, () => handleRemoveTag(tag));
349
+ }
350
+ return (_jsxs(Badge, { colorScheme: tagColorScheme, colorPalette: tagColorPalette, appearance: tagAppearance, children: [renderLabel(tag), !disabled && (_jsx(IconButton, { icon: "Times", type: "button", appearance: "none", size: "xs", style: { color: 'inherit' }, onClick: (e) => {
351
+ e.stopPropagation();
352
+ if (!disabled)
353
+ handleRemoveTag(tag);
354
+ }, "aria-label": `${t.removeTag} ${renderLabel(tag)}`, disabled: disabled, tabIndex: 0 }))] }, renderKey(tag)));
355
+ }), remainingCount > 0 && (_jsxs(Badge, { colorScheme: tagColorScheme, colorPalette: tagColorPalette, appearance: tagAppearance, children: ["+", remainingCount] }))] }));
356
+ };
357
+ const showClearButton = !disabled && ((!multiple && value !== null) ||
358
+ (multiple && value.length > 0));
359
+ return (_jsxs(CitricComponent, { tag: "div", component: "autocomplete", "data-citric": "autocomplete", style: maxHeight ? applyCSSVariable(style, 'max-height', `${maxHeight}px`) : style, className: listToClass([
360
+ className,
361
+ open && 'open',
362
+ focused && 'focused',
363
+ disabled && 'disabled',
364
+ multiple && 'multiple',
365
+ ]), ref: element, "aria-busy": loading, ...props, children: [_jsxs("header", { onClick: () => {
366
+ if (disabled)
367
+ return;
368
+ setFocused(true);
369
+ setOpen(true);
370
+ if (inputRef.current) {
371
+ inputRef.current.focus();
372
+ }
373
+ }, onFocus: () => setFocused(true), tabIndex: disabled ? undefined : 0, children: [_jsxs(Row, { gap: "4px", className: "input-container", children: [multiple && renderTags(), _jsx("input", { ref: inputRef, type: "text", value: inputValue, onChange: (e) => handleInputChange(e.target.value), onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, disabled: disabled, placeholder: multiple && value.length > 0 ? '' : placeholder, autoComplete: "off", "aria-autocomplete": "list", "aria-expanded": open, "aria-controls": "autocomplete-listbox" })] }), _jsxs("div", { className: "end-adornment", children: [loading && _jsx(ProgressCircular, { size: "xs", className: "loader" }), showClearButton && (_jsx(IconButton, { icon: "Times", appearance: "none", size: "sm", type: "button", onClick: (e) => {
374
+ e.stopPropagation();
375
+ handleClear();
376
+ }, disabled: disabled, "aria-label": t.clear, tabIndex: 1, style: { width: '12px', height: '12px' } })), _jsx(IconButton, { icon: open ? 'ChevronUp' : 'ChevronDown', appearance: "none", size: "md", type: "button", onClick: (e) => {
377
+ e.stopPropagation();
378
+ setOpen((prev) => !prev);
379
+ }, disabled: disabled, "aria-label": open ? t.collapse : t.expand, tabIndex: 1, style: { width: '12px', height: '12px' } })] })] }), _jsx("div", { className: "dropdown-panel", ref: dropdownRef, id: "autocomplete-listbox", role: "listbox", "aria-hidden": !open, ...(open ? {} : { inert: 'true' }), children: loading && !filteredOptions.length ? (_jsx("div", { className: "message", children: loadingText || t.loading })) : filteredOptions.length === 0 && !showCreateOption ? (_jsx("div", { className: "message", children: noOptionsText || t.noOptions })) : (_jsxs("div", { className: "options", children: [showCreateOption && (_jsxs("div", { role: "option", className: "option create-option", onMouseDown: (e) => {
380
+ e.preventDefault();
381
+ }, onClick: handleCreate, onMouseEnter: () => setHighlightedIndex(-1), children: [_jsx("i", { "data-citric": "icon", className: "citric-icon outline Plus" }), t.addOption.replace('{value}', inputValue)] }, "create-option")), filteredOptions.map((option, index) => (_jsxs("div", { role: "option", "aria-selected": isSelected(option), className: listToClass([
382
+ 'option',
383
+ isSelected(option) && 'selected',
384
+ highlightedIndex === index && 'highlighted',
385
+ ]), onMouseDown: (e) => {
386
+ e.preventDefault();
387
+ }, onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), children: [multiple && _jsx(Checkbox, { value: isSelected(option), readOnly: true }), renderOption ? renderOption(option) : renderLabel(option)] }, renderKey(option))))] })) })] }));
388
+ });
389
+ const dictionary = {
390
+ en: {
391
+ removeTag: 'Remove',
392
+ clear: 'Clear',
393
+ loading: 'Loading...',
394
+ noOptions: 'No options',
395
+ collapse: 'Collapse',
396
+ expand: 'Expand',
397
+ addOption: 'Add "{value}"',
398
+ },
399
+ pt: {
400
+ removeTag: 'Remover',
401
+ clear: 'Limpar',
402
+ loading: 'Carregando...',
403
+ noOptions: 'Sem opções',
404
+ collapse: 'Recolher',
405
+ expand: 'Expandir',
406
+ addOption: 'Adicionar "{value}"',
407
+ },
408
+ };
409
+ //# sourceMappingURL=Autocomplete.js.map