aplance-ui 1.3.0 → 1.4.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.
- package/package.json +1 -1
- package/registry/components/primitives/popover/popover-context.ts +33 -29
- package/registry/components/primitives/popover/popover.tsx +138 -24
- package/registry/components/ui/copy-button/copy-button.tsx +5 -0
- package/registry/components/ui/data-table/data-table.module.css +743 -710
- package/registry/components/ui/data-table/data-table.tsx +26 -8
- package/registry/components/ui/data-table/filter-modal.tsx +86 -0
- package/registry/components/ui/data-table/types.ts +8 -0
- package/registry/components/ui/data-table/useDataTable.tsx +1 -0
- package/registry/components/ui/data-table/utils.ts +26 -1
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +11 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +500 -484
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +940 -933
- package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
- package/registry/components/ui/tabs/tabs.tsx +29 -16
- package/registry/hooks/useMediaQuery.ts +38 -0
- package/registry/registry.json +22 -2
|
@@ -1,484 +1,500 @@
|
|
|
1
|
-
import type { Align, Position } from "../../../primitives/popover/popover";
|
|
2
|
-
import {
|
|
3
|
-
type CSSProperties,
|
|
4
|
-
type ReactNode,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useId,
|
|
8
|
-
useMemo,
|
|
9
|
-
useState,
|
|
10
|
-
} from "react";
|
|
11
|
-
import clsx from "clsx";
|
|
12
|
-
import { IconPlus, IconTrash } from "@tabler/icons-react";
|
|
13
|
-
import { Input } from "../input/input";
|
|
14
|
-
import {
|
|
15
|
-
Popover,
|
|
16
|
-
PopoverTrigger,
|
|
17
|
-
PopoverMenu,
|
|
18
|
-
} from "../../../primitives/popover/popover";
|
|
19
|
-
import type { ComponentProps } from "react";
|
|
20
|
-
import sCommon from "../common/input-field-common.module.css";
|
|
21
|
-
import sPickerCommon from "../common/picker-popover-common.module.css";
|
|
22
|
-
import { Button, type ButtonVariant } from "../../button/button";
|
|
23
|
-
import s from "./custom-select.module.css";
|
|
24
|
-
import { FieldMessage } from "../common/field-message/field-message";
|
|
25
|
-
import { SelectButton, type SelectButtonProps } from "../common/select-button/select-button";
|
|
26
|
-
import { Icon } from "../../../primitives/icon/icon";
|
|
27
|
-
import type { BaseInputFieldProps } from "../common/types";
|
|
28
|
-
|
|
29
|
-
export type CustomSelectOption = {
|
|
30
|
-
value: string;
|
|
31
|
-
label: string;
|
|
32
|
-
icon?: ReactNode;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
?
|
|
228
|
-
:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
)}
|
|
449
|
-
{option.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
1
|
+
import type { Align, Position } from "../../../primitives/popover/popover";
|
|
2
|
+
import {
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import clsx from "clsx";
|
|
12
|
+
import { IconPlus, IconTrash } from "@tabler/icons-react";
|
|
13
|
+
import { Input } from "../input/input";
|
|
14
|
+
import {
|
|
15
|
+
Popover,
|
|
16
|
+
PopoverTrigger,
|
|
17
|
+
PopoverMenu,
|
|
18
|
+
} from "../../../primitives/popover/popover";
|
|
19
|
+
import type { ComponentProps } from "react";
|
|
20
|
+
import sCommon from "../common/input-field-common.module.css";
|
|
21
|
+
import sPickerCommon from "../common/picker-popover-common.module.css";
|
|
22
|
+
import { Button, type ButtonVariant } from "../../button/button";
|
|
23
|
+
import s from "./custom-select.module.css";
|
|
24
|
+
import { FieldMessage } from "../common/field-message/field-message";
|
|
25
|
+
import { SelectButton, type SelectButtonProps } from "../common/select-button/select-button";
|
|
26
|
+
import { Icon } from "../../../primitives/icon/icon";
|
|
27
|
+
import type { BaseInputFieldProps } from "../common/types";
|
|
28
|
+
|
|
29
|
+
export type CustomSelectOption = {
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
icon?: ReactNode;
|
|
33
|
+
/** Trailing slot after the label and before any delete affordance. The
|
|
34
|
+
* component positions it; consumers own its visual styling. */
|
|
35
|
+
meta?: ReactNode;
|
|
36
|
+
optionStyle?: CSSProperties;
|
|
37
|
+
optionClassName?: string;
|
|
38
|
+
optionButtonVariant?: ButtonVariant;
|
|
39
|
+
applyToSelector?: boolean;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Renders a trailing delete affordance on the option row. Clicking it does
|
|
42
|
+
* NOT select the option - the consumer typically opens a confirm dialog and
|
|
43
|
+
* performs the deletion (e.g. removing a folder/tag from its pool). */
|
|
44
|
+
onDelete?: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type CustomSelectProps = Omit<
|
|
48
|
+
BaseInputFieldProps,
|
|
49
|
+
// its used in selectButton so no need it here
|
|
50
|
+
"buttonStyle"
|
|
51
|
+
> & {
|
|
52
|
+
// Select specific props
|
|
53
|
+
placeholder?: string;
|
|
54
|
+
searchPlaceholder?: string;
|
|
55
|
+
options: CustomSelectOption[];
|
|
56
|
+
value?: string | string[];
|
|
57
|
+
defaultValue?: string | string[];
|
|
58
|
+
onChange?: (value: string | string[]) => void;
|
|
59
|
+
multiple?: boolean;
|
|
60
|
+
searchable?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Lets the user create a new option from the search term when nothing in
|
|
63
|
+
* `options` matches it. Requires `searchable`. The created value is emitted
|
|
64
|
+
* through `onChange` (and added to `selectedValues`); the consumer is
|
|
65
|
+
* responsible for persisting it / pushing it into the next `options` list.
|
|
66
|
+
*/
|
|
67
|
+
allowCreate?: boolean;
|
|
68
|
+
showDisplayText?: boolean;
|
|
69
|
+
showDisplayIcon?: boolean;
|
|
70
|
+
onClear?: () => void;
|
|
71
|
+
|
|
72
|
+
// Popover behavior defaults
|
|
73
|
+
closeOnPathChange?: boolean;
|
|
74
|
+
closeOnClickOutside?: boolean;
|
|
75
|
+
animationDuration?: number;
|
|
76
|
+
position?: Position;
|
|
77
|
+
align?: Align;
|
|
78
|
+
autoPosition?: boolean;
|
|
79
|
+
collisionPadding?: number;
|
|
80
|
+
|
|
81
|
+
// Nested popover props
|
|
82
|
+
popover?: Omit<ComponentProps<typeof Popover>, "children">;
|
|
83
|
+
popoverTrigger?: ComponentProps<typeof PopoverTrigger>;
|
|
84
|
+
popoverMenu?: ComponentProps<typeof PopoverMenu>;
|
|
85
|
+
|
|
86
|
+
// Options inside menu
|
|
87
|
+
optionsContainerClassName?: string;
|
|
88
|
+
optionsContainerStyle?: CSSProperties;
|
|
89
|
+
|
|
90
|
+
// Select button props
|
|
91
|
+
selectButton?: SelectButtonProps;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const CustomSelect = ({
|
|
95
|
+
// Base props
|
|
96
|
+
label,
|
|
97
|
+
className,
|
|
98
|
+
style,
|
|
99
|
+
id,
|
|
100
|
+
info,
|
|
101
|
+
errors,
|
|
102
|
+
layout = "default",
|
|
103
|
+
|
|
104
|
+
// Select options and behavior
|
|
105
|
+
options,
|
|
106
|
+
value,
|
|
107
|
+
defaultValue,
|
|
108
|
+
onChange,
|
|
109
|
+
multiple = false,
|
|
110
|
+
searchable = false,
|
|
111
|
+
allowCreate = false,
|
|
112
|
+
showDisplayIcon = true,
|
|
113
|
+
showDisplayText = true,
|
|
114
|
+
disabled = false,
|
|
115
|
+
readOnly = false,
|
|
116
|
+
required = false,
|
|
117
|
+
onClear,
|
|
118
|
+
placeholder = "Select...",
|
|
119
|
+
searchPlaceholder = "Search options...",
|
|
120
|
+
|
|
121
|
+
// Popover behavior
|
|
122
|
+
closeOnPathChange = true,
|
|
123
|
+
closeOnClickOutside = true,
|
|
124
|
+
animationDuration = 200,
|
|
125
|
+
position = "bottom",
|
|
126
|
+
align = "start",
|
|
127
|
+
autoPosition,
|
|
128
|
+
collisionPadding,
|
|
129
|
+
|
|
130
|
+
// Menu options props
|
|
131
|
+
optionsContainerClassName,
|
|
132
|
+
optionsContainerStyle,
|
|
133
|
+
|
|
134
|
+
// Nested popover props
|
|
135
|
+
popover,
|
|
136
|
+
popoverTrigger,
|
|
137
|
+
popoverMenu,
|
|
138
|
+
selectButton,
|
|
139
|
+
}: CustomSelectProps) => {
|
|
140
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
141
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
142
|
+
|
|
143
|
+
// Internal state for uncontrolled mode
|
|
144
|
+
const [internalValue, setInternalValue] = useState<string | string[]>(() => {
|
|
145
|
+
if (defaultValue !== undefined) {
|
|
146
|
+
return Array.isArray(defaultValue)
|
|
147
|
+
? defaultValue
|
|
148
|
+
: defaultValue
|
|
149
|
+
? [defaultValue]
|
|
150
|
+
: [];
|
|
151
|
+
}
|
|
152
|
+
return multiple ? [] : "";
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const generatedId = useId();
|
|
156
|
+
const inputId = id ?? generatedId;
|
|
157
|
+
const isControlled = value !== undefined;
|
|
158
|
+
const actualValue = isControlled ? value : internalValue;
|
|
159
|
+
const interactive = !disabled && !readOnly;
|
|
160
|
+
|
|
161
|
+
// Normalize to array for consistent handling
|
|
162
|
+
const selectedValues = useMemo(
|
|
163
|
+
() =>
|
|
164
|
+
Array.isArray(actualValue)
|
|
165
|
+
? actualValue
|
|
166
|
+
: actualValue
|
|
167
|
+
? [actualValue]
|
|
168
|
+
: [],
|
|
169
|
+
[actualValue],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// A selected value may not exist in `options` - e.g. when `allowCreate` is
|
|
173
|
+
// on and the consumer just stores the value without pushing it back into
|
|
174
|
+
// the options list. Synthesize a "ghost" entry so the trigger displays it,
|
|
175
|
+
// the menu shows it (multi-select can uncheck), and the "Create" check
|
|
176
|
+
// doesn't redundantly offer something already selected.
|
|
177
|
+
const optionsWithSelected = useMemo(() => {
|
|
178
|
+
if (selectedValues.length === 0) return options;
|
|
179
|
+
const known = new Set(options.map((option) => option.value));
|
|
180
|
+
const ghosts = selectedValues
|
|
181
|
+
.filter((value) => !known.has(value))
|
|
182
|
+
.map<CustomSelectOption>((value) => ({ value, label: value }));
|
|
183
|
+
return ghosts.length > 0 ? [...ghosts, ...options] : options;
|
|
184
|
+
}, [options, selectedValues]);
|
|
185
|
+
|
|
186
|
+
// Filter options based on search
|
|
187
|
+
const filteredOptions = useMemo(
|
|
188
|
+
() =>
|
|
189
|
+
searchable && searchTerm
|
|
190
|
+
? optionsWithSelected.filter((option) =>
|
|
191
|
+
option.label.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
192
|
+
)
|
|
193
|
+
: optionsWithSelected,
|
|
194
|
+
[optionsWithSelected, searchable, searchTerm],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// "Create '<term>'" appears when allowCreate is on and the typed term
|
|
198
|
+
// doesn't match any existing option (by label or value, case-insensitive).
|
|
199
|
+
const trimmedTerm = searchTerm.trim();
|
|
200
|
+
const showCreateOption =
|
|
201
|
+
allowCreate &&
|
|
202
|
+
searchable &&
|
|
203
|
+
trimmedTerm.length > 0 &&
|
|
204
|
+
!optionsWithSelected.some(
|
|
205
|
+
(option) =>
|
|
206
|
+
option.label.toLowerCase() === trimmedTerm.toLowerCase() ||
|
|
207
|
+
option.value.toLowerCase() === trimmedTerm.toLowerCase(),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Get all selected option data in one place. `optionsWithSelected` is used
|
|
211
|
+
// for the lookup so ghost entries (selected-but-not-in-options) resolve
|
|
212
|
+
// back to a displayable label.
|
|
213
|
+
const selectedOptionData = useMemo(() => {
|
|
214
|
+
const hasValue = selectedValues.length > 0;
|
|
215
|
+
const selectedOption = optionsWithSelected.find(
|
|
216
|
+
(opt) => opt.value === selectedValues[0],
|
|
217
|
+
);
|
|
218
|
+
const isMultipleSelection = multiple && selectedValues.length > 1;
|
|
219
|
+
const singleLabel = selectedOption?.label ?? selectedValues[0];
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
hasValue,
|
|
223
|
+
option: selectedOption,
|
|
224
|
+
label: singleLabel,
|
|
225
|
+
icon: selectedOption?.icon,
|
|
226
|
+
displayText: !hasValue
|
|
227
|
+
? placeholder
|
|
228
|
+
: isMultipleSelection
|
|
229
|
+
? `${selectedValues.length} selected`
|
|
230
|
+
: singleLabel,
|
|
231
|
+
shouldShowIcon:
|
|
232
|
+
showDisplayIcon && selectedOption?.icon && selectedValues.length === 1,
|
|
233
|
+
appliedClassName: selectedOption?.applyToSelector
|
|
234
|
+
? selectedOption.optionClassName
|
|
235
|
+
: undefined,
|
|
236
|
+
appliedStyle: selectedOption?.applyToSelector
|
|
237
|
+
? selectedOption.optionStyle
|
|
238
|
+
: undefined,
|
|
239
|
+
};
|
|
240
|
+
}, [multiple, optionsWithSelected, placeholder, selectedValues, showDisplayIcon]);
|
|
241
|
+
|
|
242
|
+
const popoverOnClose = popover?.onClose;
|
|
243
|
+
const handlePopoverClose = useCallback(() => {
|
|
244
|
+
setSearchTerm("");
|
|
245
|
+
popoverOnClose?.();
|
|
246
|
+
}, [popoverOnClose]);
|
|
247
|
+
|
|
248
|
+
const {
|
|
249
|
+
className: popoverClassName,
|
|
250
|
+
offset: popoverOffset = 4,
|
|
251
|
+
autoPosition: popoverAutoPosition,
|
|
252
|
+
collisionPadding: popoverCollisionPadding,
|
|
253
|
+
...popoverRest
|
|
254
|
+
} = popover ?? {};
|
|
255
|
+
const resolvedAutoPosition = popoverAutoPosition ?? autoPosition;
|
|
256
|
+
const resolvedCollisionPadding = popoverCollisionPadding ?? collisionPadding;
|
|
257
|
+
|
|
258
|
+
const {
|
|
259
|
+
className: popoverTriggerClassName,
|
|
260
|
+
style: popoverTriggerStyle,
|
|
261
|
+
...popoverTriggerRest
|
|
262
|
+
} = popoverTrigger ?? {};
|
|
263
|
+
|
|
264
|
+
const {
|
|
265
|
+
className: popoverMenuClassName,
|
|
266
|
+
style: popoverMenuStyle,
|
|
267
|
+
...popoverMenuRest
|
|
268
|
+
} = popoverMenu ?? {};
|
|
269
|
+
|
|
270
|
+
// Handle external value changes for controlled mode
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (isControlled) {
|
|
273
|
+
const normalized = Array.isArray(value) ? value : value ? [value] : [];
|
|
274
|
+
const isDifferent =
|
|
275
|
+
normalized.length !== selectedValues.length ||
|
|
276
|
+
normalized.some((val, i) => val !== selectedValues[i]);
|
|
277
|
+
|
|
278
|
+
if (isDifferent && !normalized.length) {
|
|
279
|
+
onClear?.();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}, [value, onClear, selectedValues, isControlled]);
|
|
283
|
+
|
|
284
|
+
const handleSelect = (option: CustomSelectOption) => {
|
|
285
|
+
if (!interactive || option.disabled) return;
|
|
286
|
+
|
|
287
|
+
const optionValue = option.value;
|
|
288
|
+
|
|
289
|
+
let newValues: string | string[];
|
|
290
|
+
|
|
291
|
+
if (multiple) {
|
|
292
|
+
newValues = selectedValues.includes(optionValue)
|
|
293
|
+
? selectedValues.filter((val) => val !== optionValue)
|
|
294
|
+
: [...selectedValues, optionValue];
|
|
295
|
+
} else {
|
|
296
|
+
newValues = optionValue;
|
|
297
|
+
setIsOpen(false);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!isControlled) {
|
|
301
|
+
setInternalValue(newValues);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
onChange?.(newValues);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
308
|
+
e.stopPropagation();
|
|
309
|
+
if (!interactive) return;
|
|
310
|
+
|
|
311
|
+
const newValue = multiple ? [] : "";
|
|
312
|
+
|
|
313
|
+
if (!isControlled) {
|
|
314
|
+
setInternalValue(newValue);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
onChange?.(newValue);
|
|
318
|
+
onClear?.();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className={clsx(sCommon.container, className)} style={style}>
|
|
323
|
+
{label && (
|
|
324
|
+
<div
|
|
325
|
+
className={clsx(
|
|
326
|
+
sCommon.field_container,
|
|
327
|
+
layout !== "default" && sCommon[layout],
|
|
328
|
+
)}
|
|
329
|
+
>
|
|
330
|
+
<label className={sCommon.label} htmlFor={inputId}>
|
|
331
|
+
{label}
|
|
332
|
+
</label>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
<Popover
|
|
337
|
+
closeOnPathChange={closeOnPathChange}
|
|
338
|
+
closeOnClickOutside={closeOnClickOutside}
|
|
339
|
+
animationDuration={animationDuration}
|
|
340
|
+
isOpen={isOpen}
|
|
341
|
+
setIsOpen={setIsOpen}
|
|
342
|
+
position={position}
|
|
343
|
+
align={align}
|
|
344
|
+
autoPosition={resolvedAutoPosition}
|
|
345
|
+
collisionPadding={resolvedCollisionPadding}
|
|
346
|
+
className={clsx(sPickerCommon.picker_popover, popoverClassName)}
|
|
347
|
+
offset={popoverOffset}
|
|
348
|
+
{...popoverRest}
|
|
349
|
+
onClose={handlePopoverClose}
|
|
350
|
+
>
|
|
351
|
+
<PopoverTrigger
|
|
352
|
+
asChild
|
|
353
|
+
className={clsx(
|
|
354
|
+
sPickerCommon.picker_popover_trigger,
|
|
355
|
+
popoverTriggerClassName,
|
|
356
|
+
)}
|
|
357
|
+
style={popoverTriggerStyle}
|
|
358
|
+
disabled={disabled || readOnly}
|
|
359
|
+
{...popoverTriggerRest}
|
|
360
|
+
>
|
|
361
|
+
<SelectButton
|
|
362
|
+
disabled={disabled || readOnly}
|
|
363
|
+
required={required}
|
|
364
|
+
showClear={selectedOptionData.hasValue && interactive}
|
|
365
|
+
onClear={handleClear}
|
|
366
|
+
id={inputId}
|
|
367
|
+
className={clsx(
|
|
368
|
+
!selectedOptionData.hasValue &&
|
|
369
|
+
sPickerCommon.picker_value_placeholder,
|
|
370
|
+
selectButton?.className,
|
|
371
|
+
selectedOptionData.appliedClassName,
|
|
372
|
+
)}
|
|
373
|
+
style={{
|
|
374
|
+
...selectButton?.style,
|
|
375
|
+
...selectedOptionData.appliedStyle,
|
|
376
|
+
}}
|
|
377
|
+
iconStart={
|
|
378
|
+
selectedOptionData.shouldShowIcon ? (
|
|
379
|
+
<Icon>{selectedOptionData.icon}</Icon>
|
|
380
|
+
) : undefined
|
|
381
|
+
}
|
|
382
|
+
{...selectButton}
|
|
383
|
+
>
|
|
384
|
+
{showDisplayText && (
|
|
385
|
+
<span className={clsx(s.option_label, sCommon.truncate_text)}>
|
|
386
|
+
{selectedOptionData.displayText}
|
|
387
|
+
</span>
|
|
388
|
+
)}
|
|
389
|
+
</SelectButton>
|
|
390
|
+
</PopoverTrigger>
|
|
391
|
+
|
|
392
|
+
{!disabled && !readOnly && (
|
|
393
|
+
<PopoverMenu
|
|
394
|
+
className={clsx(
|
|
395
|
+
sPickerCommon.picker_popover_menu_base,
|
|
396
|
+
s.popover_menu,
|
|
397
|
+
popoverMenuClassName,
|
|
398
|
+
)}
|
|
399
|
+
style={popoverMenuStyle}
|
|
400
|
+
{...popoverMenuRest}
|
|
401
|
+
>
|
|
402
|
+
{searchable && (
|
|
403
|
+
<div className={s.search_container}>
|
|
404
|
+
<Input
|
|
405
|
+
type="search"
|
|
406
|
+
placeholder={searchPlaceholder}
|
|
407
|
+
value={searchTerm}
|
|
408
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
409
|
+
onClick={(e) => e.stopPropagation()}
|
|
410
|
+
showClear={true}
|
|
411
|
+
autoFocus
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
<div
|
|
417
|
+
className={clsx(s.options_container, optionsContainerClassName)}
|
|
418
|
+
style={optionsContainerStyle}
|
|
419
|
+
>
|
|
420
|
+
{showCreateOption && (
|
|
421
|
+
<Button
|
|
422
|
+
as="div"
|
|
423
|
+
variant="ghost"
|
|
424
|
+
className={s.option}
|
|
425
|
+
onClick={() => {
|
|
426
|
+
handleSelect({ value: trimmedTerm, label: trimmedTerm });
|
|
427
|
+
setSearchTerm("");
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
<Icon size="sm" aria-hidden="true">
|
|
431
|
+
<IconPlus />
|
|
432
|
+
</Icon>
|
|
433
|
+
<span
|
|
434
|
+
className={clsx(s.option_label, sCommon.truncate_text)}
|
|
435
|
+
>
|
|
436
|
+
Create “{trimmedTerm}”
|
|
437
|
+
</span>
|
|
438
|
+
</Button>
|
|
439
|
+
)}
|
|
440
|
+
{filteredOptions.length === 0 && !showCreateOption ? (
|
|
441
|
+
<p className={s.no_options}>No options found</p>
|
|
442
|
+
) : (
|
|
443
|
+
filteredOptions.map((option) => (
|
|
444
|
+
<Button
|
|
445
|
+
key={option.value}
|
|
446
|
+
as="div"
|
|
447
|
+
variant={option.optionButtonVariant || "ghost"}
|
|
448
|
+
className={clsx(s.option, option.optionClassName)}
|
|
449
|
+
style={option.optionStyle}
|
|
450
|
+
isActive={selectedValues.includes(option.value)}
|
|
451
|
+
disabled={disabled || readOnly || option.disabled}
|
|
452
|
+
onClick={() => handleSelect(option)}
|
|
453
|
+
>
|
|
454
|
+
{multiple && (
|
|
455
|
+
<Input
|
|
456
|
+
type="checkbox"
|
|
457
|
+
className={sCommon.option_checkbox}
|
|
458
|
+
checked={selectedValues.includes(option.value)}
|
|
459
|
+
disabled={disabled || readOnly || option.disabled}
|
|
460
|
+
/>
|
|
461
|
+
)}
|
|
462
|
+
{option.icon && option.icon}
|
|
463
|
+
<span
|
|
464
|
+
className={clsx(s.option_label, sCommon.truncate_text)}
|
|
465
|
+
>
|
|
466
|
+
{option.label}
|
|
467
|
+
</span>
|
|
468
|
+
{option.meta != null ? (
|
|
469
|
+
<span className={s.option_meta}>{option.meta}</span>
|
|
470
|
+
) : null}
|
|
471
|
+
{option.onDelete && interactive ? (
|
|
472
|
+
<Icon
|
|
473
|
+
size="sm"
|
|
474
|
+
clickable
|
|
475
|
+
className={s.option_delete}
|
|
476
|
+
aria-label={`Delete ${option.label}`}
|
|
477
|
+
onClick={(e) => {
|
|
478
|
+
e.stopPropagation();
|
|
479
|
+
// Close the popover BEFORE handing off - the consumer
|
|
480
|
+
// opens a confirm dialog, which must not sit under
|
|
481
|
+
// the still-open popover in the stacking order.
|
|
482
|
+
setIsOpen(false);
|
|
483
|
+
option.onDelete?.();
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
<IconTrash />
|
|
487
|
+
</Icon>
|
|
488
|
+
) : null}
|
|
489
|
+
</Button>
|
|
490
|
+
))
|
|
491
|
+
)}
|
|
492
|
+
</div>
|
|
493
|
+
</PopoverMenu>
|
|
494
|
+
)}
|
|
495
|
+
</Popover>
|
|
496
|
+
|
|
497
|
+
<FieldMessage errors={errors} info={info} />
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
};
|