aplance-ui 1.4.0 → 1.5.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.
- package/package.json +1 -1
- package/registry/components/primitives/icon/icon.tsx +4 -6
- package/registry/components/primitives/modal/modal.module.css +5 -0
- package/registry/components/primitives/modal/modal.tsx +32 -34
- package/registry/components/primitives/popover/popover.module.css +4 -2
- package/registry/components/primitives/popover/popover.tsx +16 -17
- package/registry/components/primitives/scroll-rail/scroll-rail.module.css +148 -0
- package/registry/components/primitives/scroll-rail/scroll-rail.tsx +214 -0
- package/registry/components/ui/accordion/accordion.module.css +52 -0
- package/registry/components/ui/accordion/accordion.tsx +97 -25
- package/registry/components/ui/avatar/avatar.tsx +2 -2
- package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +9 -9
- package/registry/components/ui/button/button.module.css +43 -5
- package/registry/components/ui/button/button.tsx +2 -1
- package/registry/components/ui/copy-button/copy-button.module.css +2 -0
- package/registry/components/ui/data-table/data-table.module.css +68 -25
- package/registry/components/ui/data-table/data-table.tsx +1238 -1229
- package/registry/components/ui/data-table/filters.tsx +21 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +31 -17
- package/registry/components/ui/data-table/types.ts +11 -1
- package/registry/components/ui/data-table/useDataTable.tsx +1 -2
- package/registry/components/ui/data-table/utils.ts +4 -3
- package/registry/components/ui/input-fields/common/input-field-common.module.css +8 -2
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +4 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +115 -84
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +63 -11
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +51 -55
- package/registry/components/ui/input-fields/input/input.tsx +348 -348
- package/registry/components/ui/input-fields/media-input/media-input.tsx +3 -1
- package/registry/components/ui/input-fields/media-input/types.ts +8 -2
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +5 -5
- package/registry/components/ui/input-fields/media-input/utils.ts +18 -5
- package/registry/components/ui/tabs/tabs.module.css +8 -12
- package/registry/components/ui/tabs/tabs.tsx +75 -44
- package/registry/components/ui/tooltip/tooltip.module.css +8 -3
- package/registry/components/ui/tooltip/tooltip.tsx +16 -3
- package/registry/lib/string.ts +3 -3
- package/registry/registry.json +32 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
|
2
|
+
import { FilterBar } from "./filter-bar";
|
|
3
|
+
import { FilterModal } from "./filter-modal";
|
|
4
|
+
import type { FilterBarProps } from "./types";
|
|
5
|
+
|
|
6
|
+
// Below this width an inline filter row can't fit its fields (matches the
|
|
7
|
+
// `--md` custom-media token in styles/media.css): the filters collapse into a
|
|
8
|
+
// single "Filters" button and a modal instead of stacking full-height.
|
|
9
|
+
const FILTER_COMPACT_QUERY = "(max-width: 900px)";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A list's filters: the inline <FilterBar> while it fits, the <FilterModal>
|
|
13
|
+
* on a narrower screen. DataTable shows its filters through it; a list built
|
|
14
|
+
* on its own uses it the same way.
|
|
15
|
+
*/
|
|
16
|
+
export const Filters = (props: FilterBarProps) =>
|
|
17
|
+
useMediaQuery(FILTER_COMPACT_QUERY) ? (
|
|
18
|
+
<FilterModal {...props} />
|
|
19
|
+
) : (
|
|
20
|
+
<FilterBar {...props} />
|
|
21
|
+
);
|
|
@@ -8,6 +8,7 @@ import clsx from "clsx";
|
|
|
8
8
|
import { Button } from "../button/button";
|
|
9
9
|
import { CustomSelect } from "../input-fields/custom-select/custom-select";
|
|
10
10
|
import { Loader } from "../loader/loader";
|
|
11
|
+
import { ScrollRail } from "../../primitives/scroll-rail/scroll-rail";
|
|
11
12
|
import type { PaginationBarProps } from "./types";
|
|
12
13
|
import s from "./data-table.module.css";
|
|
13
14
|
|
|
@@ -28,6 +29,7 @@ export const PaginationBar = <TData,>({
|
|
|
28
29
|
rowLabel,
|
|
29
30
|
rowsSelectable,
|
|
30
31
|
selectedCount,
|
|
32
|
+
label,
|
|
31
33
|
extra,
|
|
32
34
|
}: PaginationBarProps<TData>) => {
|
|
33
35
|
const pageCount = table.getPageCount();
|
|
@@ -42,6 +44,7 @@ export const PaginationBar = <TData,>({
|
|
|
42
44
|
const pageNavItems = [
|
|
43
45
|
{
|
|
44
46
|
key: "first",
|
|
47
|
+
edge: true,
|
|
45
48
|
label: "First page",
|
|
46
49
|
icon: <IconChevronsLeft size={14} stroke={2} />,
|
|
47
50
|
onClick: () => goTo(0),
|
|
@@ -63,6 +66,7 @@ export const PaginationBar = <TData,>({
|
|
|
63
66
|
},
|
|
64
67
|
{
|
|
65
68
|
key: "last",
|
|
69
|
+
edge: true,
|
|
66
70
|
label: "Last page",
|
|
67
71
|
icon: <IconChevronsRight size={14} stroke={2} />,
|
|
68
72
|
onClick: () => goTo(pageCount - 1),
|
|
@@ -111,11 +115,9 @@ export const PaginationBar = <TData,>({
|
|
|
111
115
|
);
|
|
112
116
|
|
|
113
117
|
return (
|
|
114
|
-
<div
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
{/* left - count + optional bulk-action extra slot */}
|
|
118
|
+
<div className={s.pagination_bar} aria-busy={isLoading || undefined}>
|
|
119
|
+
{/* left - the count (or the caller's label), then the extra slot,
|
|
120
|
+
which scrolls on its own while the count stays put */}
|
|
119
121
|
<div
|
|
120
122
|
className={clsx(
|
|
121
123
|
s.pagination_left,
|
|
@@ -131,18 +133,26 @@ export const PaginationBar = <TData,>({
|
|
|
131
133
|
) : (
|
|
132
134
|
<>
|
|
133
135
|
<p className={s.pagination_count}>
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
{label ??
|
|
137
|
+
(rowsSelectable && selectedCount > 0 ? (
|
|
138
|
+
<>
|
|
139
|
+
<span>{selectedCount}</span> of <span>{total}</span>{" "}
|
|
140
|
+
{rowLabel}(s) selected
|
|
141
|
+
</>
|
|
142
|
+
) : (
|
|
143
|
+
<>
|
|
144
|
+
<span>{total}</span> {rowLabel}(s)
|
|
145
|
+
</>
|
|
146
|
+
))}
|
|
144
147
|
</p>
|
|
145
|
-
{extra
|
|
148
|
+
{extra ? (
|
|
149
|
+
<ScrollRail
|
|
150
|
+
className={s.pagination_extra}
|
|
151
|
+
contentClassName={s.pagination_extra_items}
|
|
152
|
+
>
|
|
153
|
+
{extra}
|
|
154
|
+
</ScrollRail>
|
|
155
|
+
) : null}
|
|
146
156
|
</>
|
|
147
157
|
)}
|
|
148
158
|
</div>
|
|
@@ -205,7 +215,10 @@ export const PaginationBar = <TData,>({
|
|
|
205
215
|
<Loader
|
|
206
216
|
key={`${item.key}-skeleton`}
|
|
207
217
|
variant="skeleton"
|
|
208
|
-
className={
|
|
218
|
+
className={clsx(
|
|
219
|
+
s.pagination_button_skeleton,
|
|
220
|
+
item.edge && s.page_nav_edge,
|
|
221
|
+
)}
|
|
209
222
|
/>
|
|
210
223
|
))
|
|
211
224
|
: pageNavItems.map((item) => (
|
|
@@ -213,6 +226,7 @@ export const PaginationBar = <TData,>({
|
|
|
213
226
|
key={item.key}
|
|
214
227
|
variant="bordered"
|
|
215
228
|
layout="box"
|
|
229
|
+
className={item.edge ? s.page_nav_edge : undefined}
|
|
216
230
|
onClick={item.onClick}
|
|
217
231
|
disabled={item.disabled}
|
|
218
232
|
aria-label={item.label}
|
|
@@ -159,6 +159,7 @@ export interface PaginationBarProps<TData> {
|
|
|
159
159
|
rowLabel: string;
|
|
160
160
|
rowsSelectable: boolean;
|
|
161
161
|
selectedCount: number;
|
|
162
|
+
label: ReactNode;
|
|
162
163
|
extra: ReactNode;
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -298,10 +299,19 @@ export interface DataTableProps<TData extends { id: string }> {
|
|
|
298
299
|
|
|
299
300
|
/**
|
|
300
301
|
* Slot rendered inside the pagination bar next to the count text.
|
|
301
|
-
* Perfect for bulk-action buttons. Does not shift table layout
|
|
302
|
+
* Perfect for bulk-action buttons. Does not shift table layout: when it
|
|
303
|
+
* does not fit, it scrolls across on its own (a ScrollRail) while the count
|
|
304
|
+
* stays put.
|
|
302
305
|
*/
|
|
303
306
|
paginationExtra?: ReactNode;
|
|
304
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Replaces the pagination bar's count text, e.g. for a selection that
|
|
310
|
+
* reaches past the loaded rows ("All 120 files selected"). Numbers wrapped
|
|
311
|
+
* in a `<span>` are emphasised like the default count's.
|
|
312
|
+
*/
|
|
313
|
+
paginationLabel?: ReactNode;
|
|
314
|
+
|
|
305
315
|
className?: string;
|
|
306
316
|
}
|
|
307
317
|
|
|
@@ -260,8 +260,7 @@ export function useDataTable<TData extends { id: string }>({
|
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
const next = [...prev];
|
|
263
|
-
|
|
264
|
-
next.splice(targetIdx, 0, moved);
|
|
263
|
+
next.splice(targetIdx, 0, ...next.splice(draggedIdx, 1));
|
|
265
264
|
localDataRef.current = next;
|
|
266
265
|
setLocalData(next);
|
|
267
266
|
onDataChange?.(next);
|
|
@@ -358,9 +358,10 @@ export function writeSearchParams(
|
|
|
358
358
|
params.delete(p(PARAM_KEYS.pageSize));
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
params.set(p(PARAM_KEYS.
|
|
361
|
+
const sort = state.sorting?.[0];
|
|
362
|
+
if (sort) {
|
|
363
|
+
params.set(p(PARAM_KEYS.sortBy), sort.id);
|
|
364
|
+
params.set(p(PARAM_KEYS.sortDir), sort.desc ? "desc" : "asc");
|
|
364
365
|
} else {
|
|
365
366
|
params.delete(p(PARAM_KEYS.sortBy));
|
|
366
367
|
params.delete(p(PARAM_KEYS.sortDir));
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
.container {
|
|
1
|
+
.container {
|
|
2
2
|
width: 100%;
|
|
3
|
-
|
|
3
|
+
/* A field takes the width its layout gives it, never its content's: a long
|
|
4
|
+
value truncates instead of pushing the field over its neighbour in a grid
|
|
5
|
+
column or a flex row. */
|
|
6
|
+
min-width: 0;
|
|
4
7
|
display: flex;
|
|
5
8
|
flex-direction: column;
|
|
6
9
|
gap: var(--spacing-2);
|
|
@@ -248,6 +251,9 @@
|
|
|
248
251
|
Keep these after .field_default so they reliably override base paddings. */
|
|
249
252
|
.field_full_width {
|
|
250
253
|
width: 100% !important;
|
|
254
|
+
/* Fills the field and no more: a button is otherwise never narrower than
|
|
255
|
+
its label (`--button-min-width`), which would defeat `.truncate_text`. */
|
|
256
|
+
--button-min-width: 0;
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
.field_wrapper_fit {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
.picker_popover_trigger {
|
|
12
12
|
width: 100%;
|
|
13
|
+
/* A field's trigger takes the field's width, never its value's: Popover
|
|
14
|
+
keeps a trigger at least as wide as its content (right for a plain
|
|
15
|
+
button), which would push a long value over the next field. */
|
|
16
|
+
--popover-trigger-min-width: 0;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
.picker_popover_menu_base {
|
|
@@ -1,87 +1,118 @@
|
|
|
1
|
-
/* uses input-field-common (.container and all)
|
|
2
|
-
+ popover related stuff
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/* actual menu selector */
|
|
6
|
-
.popover_menu {
|
|
7
|
-
--picker-popover-menu-width: 100%;
|
|
8
|
-
width: 100%;
|
|
9
|
-
border-radius: calc(var(--radius-md) + var(--spacing-2));
|
|
10
|
-
|
|
11
|
-
/* Search container */
|
|
12
|
-
.search_container {
|
|
13
|
-
padding: var(--spacing-4);
|
|
14
|
-
border-bottom: 1px solid var(--color-border);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* Options container */
|
|
18
|
-
.options_container {
|
|
19
|
-
width: 100%;
|
|
20
|
-
max-height: 16rem;
|
|
21
|
-
overflow-y: auto;
|
|
22
|
-
padding: var(--spacing-2);
|
|
23
|
-
display: flex;
|
|
24
|
-
flex-direction: column;
|
|
25
|
-
gap: var(--spacing-2);
|
|
26
|
-
|
|
27
|
-
.no_options {
|
|
28
|
-
padding: var(--spacing-4);
|
|
29
|
-
text-align: center;
|
|
30
|
-
color: var(--color-muted-foreground);
|
|
31
|
-
font-size: var(--font-size-sm);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* Option button */
|
|
35
|
-
.option {
|
|
36
|
-
width: 100%;
|
|
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
|
-
|
|
1
|
+
/* uses input-field-common (.container and all)
|
|
2
|
+
+ popover related stuff
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* actual menu selector */
|
|
6
|
+
.popover_menu {
|
|
7
|
+
--picker-popover-menu-width: 100%;
|
|
8
|
+
width: 100%;
|
|
9
|
+
border-radius: calc(var(--radius-md) + var(--spacing-2));
|
|
10
|
+
|
|
11
|
+
/* Search container */
|
|
12
|
+
.search_container {
|
|
13
|
+
padding: var(--spacing-4);
|
|
14
|
+
border-bottom: 1px solid var(--color-border);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Options container */
|
|
18
|
+
.options_container {
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-height: 16rem;
|
|
21
|
+
overflow-y: auto;
|
|
22
|
+
padding: var(--spacing-2);
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: var(--spacing-2);
|
|
26
|
+
|
|
27
|
+
.no_options {
|
|
28
|
+
padding: var(--spacing-4);
|
|
29
|
+
text-align: center;
|
|
30
|
+
color: var(--color-muted-foreground);
|
|
31
|
+
font-size: var(--font-size-sm);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Option button */
|
|
35
|
+
.option {
|
|
36
|
+
width: 100%;
|
|
37
|
+
/* The list scrolls; an option keeps its height. The button base sets a
|
|
38
|
+
min-height, which would otherwise let a long list squeeze each option
|
|
39
|
+
down to it, its text spilling over the next. */
|
|
40
|
+
flex-shrink: 0;
|
|
41
|
+
/* button base is min-width: fit-content - a long label must truncate
|
|
42
|
+
inside the row, never widen it past the menu */
|
|
43
|
+
min-width: 0;
|
|
44
|
+
/* button has justify-content: space-between */
|
|
45
|
+
justify-content: flex-start;
|
|
46
|
+
/* modifying padding of actual button variant for consistency */
|
|
47
|
+
padding: var(--spacing-3) var(--spacing-4);
|
|
48
|
+
border-radius: var(--radius-md);
|
|
49
|
+
|
|
50
|
+
/* > icon */
|
|
51
|
+
|
|
52
|
+
/* The label owns the row's slack space so trailing affordances
|
|
53
|
+
(.option_delete) always sit at the row's end - layout-driven, no
|
|
54
|
+
auto-margins (those tie with the Icon primitive's own margin reset
|
|
55
|
+
and the winner would depend on CSS bundle order). */
|
|
56
|
+
.option_label {
|
|
57
|
+
flex: 1 1 auto;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
text-align: start;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* With a description: two lines, so more room around them, and what
|
|
63
|
+
leads the row (a checkbox, an icon) sits at the label's line. */
|
|
64
|
+
&.option_rich {
|
|
65
|
+
padding-block: var(--spacing-5);
|
|
66
|
+
align-items: flex-start;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.option_label {
|
|
73
|
+
max-width: 100% !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* An option with a description: its label over a line saying what it does.
|
|
77
|
+
The pair takes the row's slack, as a label alone does, and the line wraps:
|
|
78
|
+
the button base keeps its text on one line. */
|
|
79
|
+
.option_text {
|
|
80
|
+
flex: 1 1 auto;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
gap: var(--spacing-2);
|
|
85
|
+
text-align: start;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.option_description {
|
|
89
|
+
color: var(--color-muted-foreground);
|
|
90
|
+
font-size: var(--font-size-xs);
|
|
91
|
+
font-weight: var(--font-weight-normal);
|
|
92
|
+
line-height: var(--line-height-md);
|
|
93
|
+
white-space: normal;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
/* Trailing per-option meta (option.meta). The growing .option_label pushes it
|
|
66
97
|
to the row's end; consumers own the slot's visual styling. */
|
|
67
98
|
.option_meta {
|
|
68
|
-
flex-shrink: 0;
|
|
69
|
-
display: inline-flex;
|
|
70
|
-
align-items: center;
|
|
71
|
-
gap: var(--spacing-2);
|
|
72
|
-
margin-inline-start: var(--spacing-3);
|
|
73
|
-
white-space: nowrap;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* Trailing per-option delete affordance (option.onDelete). The growing
|
|
77
|
-
.option_label pushes it to the row's end; muted until hovered, then
|
|
78
|
-
error-toned. Click is stopPropagation'd in the component so it never
|
|
79
|
-
selects the option. */
|
|
80
|
-
.option_delete {
|
|
81
|
-
flex-shrink: 0;
|
|
82
|
-
--icon-color: var(--color-muted-foreground);
|
|
83
|
-
|
|
84
|
-
&:hover {
|
|
85
|
-
--icon-color: var(--color-error-foreground);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: var(--spacing-2);
|
|
103
|
+
margin-inline-start: var(--spacing-3);
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Trailing per-option delete affordance (option.onDelete). The growing
|
|
108
|
+
.option_label pushes it to the row's end; muted until hovered, then
|
|
109
|
+
error-toned. Click is stopPropagation'd in the component so it never
|
|
110
|
+
selects the option. */
|
|
111
|
+
.option_delete {
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
--icon-color: var(--color-muted-foreground);
|
|
114
|
+
|
|
115
|
+
&:hover {
|
|
116
|
+
--icon-color: var(--color-error-foreground);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -29,6 +29,10 @@ import type { BaseInputFieldProps } from "../common/types";
|
|
|
29
29
|
export type CustomSelectOption = {
|
|
30
30
|
value: string;
|
|
31
31
|
label: string;
|
|
32
|
+
/** A line under the label in the menu, for an option its label alone does
|
|
33
|
+
* not explain. It wraps, never truncates, and search matches it too; once
|
|
34
|
+
* chosen, the select shows the label only. */
|
|
35
|
+
description?: string;
|
|
32
36
|
icon?: ReactNode;
|
|
33
37
|
/** Trailing slot after the label and before any delete affordance. The
|
|
34
38
|
* component positions it; consumers own its visual styling. */
|
|
@@ -67,6 +71,10 @@ export type CustomSelectProps = Omit<
|
|
|
67
71
|
allowCreate?: boolean;
|
|
68
72
|
showDisplayText?: boolean;
|
|
69
73
|
showDisplayIcon?: boolean;
|
|
74
|
+
/** What the select shows once something is chosen, in place of the chosen
|
|
75
|
+
* label or the count: "All images" for a choice that stands for every
|
|
76
|
+
* option. */
|
|
77
|
+
valueText?: string;
|
|
70
78
|
onClear?: () => void;
|
|
71
79
|
|
|
72
80
|
// Popover behavior defaults
|
|
@@ -111,6 +119,7 @@ export const CustomSelect = ({
|
|
|
111
119
|
allowCreate = false,
|
|
112
120
|
showDisplayIcon = true,
|
|
113
121
|
showDisplayText = true,
|
|
122
|
+
valueText,
|
|
114
123
|
disabled = false,
|
|
115
124
|
readOnly = false,
|
|
116
125
|
required = false,
|
|
@@ -188,7 +197,9 @@ export const CustomSelect = ({
|
|
|
188
197
|
() =>
|
|
189
198
|
searchable && searchTerm
|
|
190
199
|
? optionsWithSelected.filter((option) =>
|
|
191
|
-
option.label.
|
|
200
|
+
[option.label, option.description ?? ""].some((text) =>
|
|
201
|
+
text.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
202
|
+
),
|
|
192
203
|
)
|
|
193
204
|
: optionsWithSelected,
|
|
194
205
|
[optionsWithSelected, searchable, searchTerm],
|
|
@@ -225,9 +236,10 @@ export const CustomSelect = ({
|
|
|
225
236
|
icon: selectedOption?.icon,
|
|
226
237
|
displayText: !hasValue
|
|
227
238
|
? placeholder
|
|
228
|
-
:
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
: (valueText ??
|
|
240
|
+
(isMultipleSelection
|
|
241
|
+
? `${selectedValues.length} selected`
|
|
242
|
+
: singleLabel)),
|
|
231
243
|
shouldShowIcon:
|
|
232
244
|
showDisplayIcon && selectedOption?.icon && selectedValues.length === 1,
|
|
233
245
|
appliedClassName: selectedOption?.applyToSelector
|
|
@@ -237,7 +249,7 @@ export const CustomSelect = ({
|
|
|
237
249
|
? selectedOption.optionStyle
|
|
238
250
|
: undefined,
|
|
239
251
|
};
|
|
240
|
-
}, [multiple, optionsWithSelected, placeholder, selectedValues, showDisplayIcon]);
|
|
252
|
+
}, [multiple, optionsWithSelected, placeholder, selectedValues, showDisplayIcon, valueText]);
|
|
241
253
|
|
|
242
254
|
const popoverOnClose = popover?.onClose;
|
|
243
255
|
const handlePopoverClose = useCallback(() => {
|
|
@@ -304,6 +316,25 @@ export const CustomSelect = ({
|
|
|
304
316
|
onChange?.(newValues);
|
|
305
317
|
};
|
|
306
318
|
|
|
319
|
+
// Enter in the search box picks the first row: the Create row while one
|
|
320
|
+
// shows, else the first option found. A choice already made stays made.
|
|
321
|
+
const handleSearchKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
322
|
+
if (e.key !== "Enter") return;
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
if (!trimmedTerm) return;
|
|
325
|
+
if (showCreateOption) {
|
|
326
|
+
handleSelect({ value: trimmedTerm, label: trimmedTerm });
|
|
327
|
+
} else {
|
|
328
|
+
const first = filteredOptions.find(
|
|
329
|
+
(option) =>
|
|
330
|
+
!option.disabled &&
|
|
331
|
+
!(multiple && selectedValues.includes(option.value)),
|
|
332
|
+
);
|
|
333
|
+
if (first) handleSelect(first);
|
|
334
|
+
}
|
|
335
|
+
setSearchTerm("");
|
|
336
|
+
};
|
|
337
|
+
|
|
307
338
|
const handleClear = (e: React.MouseEvent) => {
|
|
308
339
|
e.stopPropagation();
|
|
309
340
|
if (!interactive) return;
|
|
@@ -406,6 +437,7 @@ export const CustomSelect = ({
|
|
|
406
437
|
placeholder={searchPlaceholder}
|
|
407
438
|
value={searchTerm}
|
|
408
439
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
440
|
+
onKeyDown={handleSearchKey}
|
|
409
441
|
onClick={(e) => e.stopPropagation()}
|
|
410
442
|
showClear={true}
|
|
411
443
|
autoFocus
|
|
@@ -445,7 +477,11 @@ export const CustomSelect = ({
|
|
|
445
477
|
key={option.value}
|
|
446
478
|
as="div"
|
|
447
479
|
variant={option.optionButtonVariant || "ghost"}
|
|
448
|
-
className={clsx(
|
|
480
|
+
className={clsx(
|
|
481
|
+
s.option,
|
|
482
|
+
option.description && s.option_rich,
|
|
483
|
+
option.optionClassName,
|
|
484
|
+
)}
|
|
449
485
|
style={option.optionStyle}
|
|
450
486
|
isActive={selectedValues.includes(option.value)}
|
|
451
487
|
disabled={disabled || readOnly || option.disabled}
|
|
@@ -460,11 +496,27 @@ export const CustomSelect = ({
|
|
|
460
496
|
/>
|
|
461
497
|
)}
|
|
462
498
|
{option.icon && option.icon}
|
|
463
|
-
|
|
464
|
-
className={
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
499
|
+
{option.description ? (
|
|
500
|
+
<span className={s.option_text}>
|
|
501
|
+
<span
|
|
502
|
+
className={clsx(
|
|
503
|
+
s.option_label,
|
|
504
|
+
sCommon.truncate_text,
|
|
505
|
+
)}
|
|
506
|
+
>
|
|
507
|
+
{option.label}
|
|
508
|
+
</span>
|
|
509
|
+
<span className={s.option_description}>
|
|
510
|
+
{option.description}
|
|
511
|
+
</span>
|
|
512
|
+
</span>
|
|
513
|
+
) : (
|
|
514
|
+
<span
|
|
515
|
+
className={clsx(s.option_label, sCommon.truncate_text)}
|
|
516
|
+
>
|
|
517
|
+
{option.label}
|
|
518
|
+
</span>
|
|
519
|
+
)}
|
|
468
520
|
{option.meta != null ? (
|
|
469
521
|
<span className={s.option_meta}>{option.meta}</span>
|
|
470
522
|
) : null}
|