@jinaan/uikit 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/data-table.d.ts +17 -1
- package/dist/data-table.d.ts.map +1 -1
- package/dist/index.js +857 -726
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +2 -0
- package/dist/input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/data-table.tsx +194 -30
- package/src/input.tsx +32 -8
package/dist/input.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
|
|
|
7
7
|
leadingIcon?: ReactNode;
|
|
8
8
|
/** Optional icon rendered inside the field, after the text. */
|
|
9
9
|
trailingIcon?: ReactNode;
|
|
10
|
+
/** Shows a real clear button when the field has a value. */
|
|
11
|
+
onClear?: () => void;
|
|
10
12
|
/** Error styling; also pairs with `helperText` for the message. */
|
|
11
13
|
error?: boolean;
|
|
12
14
|
/** Helper or error message shown below the field (wired to aria-describedby). */
|
package/dist/input.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.tsx"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,mBAAmB,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIpF,KAAK,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAqC3C,MAAM,WAAW,UACf,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC3D,+CAA+C;IAC/C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,gEAAgE;IAChE,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,KAAK,yGAqIjB,CAAC"}
|
package/package.json
CHANGED
package/src/data-table.tsx
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
CaretUp,
|
|
4
4
|
CaretUpDown,
|
|
5
5
|
Columns as ColumnsIcon,
|
|
6
|
+
FunnelSimple,
|
|
7
|
+
Lock,
|
|
6
8
|
MagnifyingGlass,
|
|
7
9
|
WarningCircle,
|
|
8
10
|
X,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
DropdownMenu,
|
|
18
20
|
DropdownMenuCheckboxItem,
|
|
19
21
|
DropdownMenuContent,
|
|
22
|
+
DropdownMenuItem,
|
|
20
23
|
DropdownMenuLabel,
|
|
21
24
|
DropdownMenuSeparator,
|
|
22
25
|
DropdownMenuTrigger,
|
|
@@ -49,6 +52,23 @@ export interface Column<T> {
|
|
|
49
52
|
className?: string;
|
|
50
53
|
/** Column width for the fixed layout (e.g. "18rem", "20%"). Defaults to auto. */
|
|
51
54
|
width?: string;
|
|
55
|
+
/** Required columns stay visible and cannot be hidden. */
|
|
56
|
+
required?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DataTableFilterOption {
|
|
60
|
+
value: string;
|
|
61
|
+
label: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DataTableFilter<T> {
|
|
65
|
+
id: string;
|
|
66
|
+
label: string;
|
|
67
|
+
options: DataTableFilterOption[];
|
|
68
|
+
value: string[];
|
|
69
|
+
onChange: (value: string[]) => void;
|
|
70
|
+
/** Return true when the row matches the selected filter values. */
|
|
71
|
+
match: (row: T, values: string[]) => boolean;
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
export interface BulkAction<T> {
|
|
@@ -90,6 +110,7 @@ interface DataTableProps<T> {
|
|
|
90
110
|
placeholder?: string;
|
|
91
111
|
keys: (row: T) => string;
|
|
92
112
|
};
|
|
113
|
+
filters?: DataTableFilter<T>[];
|
|
93
114
|
toolbarStart?: ReactNode;
|
|
94
115
|
toolbarEnd?: ReactNode;
|
|
95
116
|
|
|
@@ -124,6 +145,9 @@ interface DataTableProps<T> {
|
|
|
124
145
|
const HEADER_CELL =
|
|
125
146
|
"h-[var(--table-header-h)] whitespace-nowrap px-[var(--table-pad-x)] text-left align-middle text-[0.8125rem] font-medium";
|
|
126
147
|
|
|
148
|
+
const TOOLBAR_BUTTON =
|
|
149
|
+
"rounded-[6px] bg-surface text-fg shadow-xs ring-1 ring-line hover:bg-elevated hover:text-fg active:bg-elevated";
|
|
150
|
+
|
|
127
151
|
function initials(name: string): string {
|
|
128
152
|
return name
|
|
129
153
|
.split(/\s+/)
|
|
@@ -164,6 +188,7 @@ export function DataTable<T>({
|
|
|
164
188
|
error,
|
|
165
189
|
empty,
|
|
166
190
|
search,
|
|
191
|
+
filters,
|
|
167
192
|
toolbarStart,
|
|
168
193
|
toolbarEnd,
|
|
169
194
|
tabs,
|
|
@@ -184,7 +209,9 @@ export function DataTable<T>({
|
|
|
184
209
|
className,
|
|
185
210
|
}: DataTableProps<T>) {
|
|
186
211
|
const [hidden, setHidden] = useState<Record<string, boolean>>(() =>
|
|
187
|
-
Object.fromEntries(
|
|
212
|
+
Object.fromEntries(
|
|
213
|
+
columns.filter((c) => c.defaultHidden && !c.required).map((c) => [c.id, true]),
|
|
214
|
+
),
|
|
188
215
|
);
|
|
189
216
|
const [sort, setSort] = useState<{ id: string; dir: "asc" | "desc" } | null>(
|
|
190
217
|
defaultSort ?? null,
|
|
@@ -192,14 +219,26 @@ export function DataTable<T>({
|
|
|
192
219
|
const [page, setPage] = useState(0);
|
|
193
220
|
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
194
221
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
222
|
+
const [activeFilterIds, setActiveFilterIds] = useState<string[]>([]);
|
|
195
223
|
|
|
196
224
|
const visibleColumns = columns.filter((c) => !hidden[c.id]);
|
|
225
|
+
const hiddenColumns = columns.filter((c) => hidden[c.id] && !c.required);
|
|
226
|
+
const activeFilters = (filters ?? []).filter((f) => activeFilterIds.includes(f.id));
|
|
227
|
+
const filterSignature = (filters ?? [])
|
|
228
|
+
.map((f) => `${f.id}:${f.value.join(",")}`)
|
|
229
|
+
.join("|");
|
|
197
230
|
|
|
198
231
|
const filtered = useMemo(() => {
|
|
199
|
-
|
|
232
|
+
let next = rows;
|
|
233
|
+
for (const filter of filters ?? []) {
|
|
234
|
+
if (filter.value.length > 0) {
|
|
235
|
+
next = next.filter((row) => filter.match(row, filter.value));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!search || !search.value.trim()) return next;
|
|
200
239
|
const q = search.value.trim().toLowerCase();
|
|
201
|
-
return
|
|
202
|
-
}, [rows, search]);
|
|
240
|
+
return next.filter((r) => search.keys(r).toLowerCase().includes(q));
|
|
241
|
+
}, [rows, search, filters]);
|
|
203
242
|
|
|
204
243
|
const sorted = useMemo(() => {
|
|
205
244
|
if (!sort) return filtered;
|
|
@@ -221,7 +260,7 @@ export function DataTable<T>({
|
|
|
221
260
|
|
|
222
261
|
useEffect(() => {
|
|
223
262
|
setPage(0);
|
|
224
|
-
}, [search?.value, pageSize, sort?.id, sort?.dir, activeTab]);
|
|
263
|
+
}, [search?.value, pageSize, sort?.id, sort?.dir, activeTab, filterSignature]);
|
|
225
264
|
|
|
226
265
|
useEffect(() => {
|
|
227
266
|
if (page > pageCount - 1) setPage(pageCount - 1);
|
|
@@ -263,7 +302,7 @@ export function DataTable<T>({
|
|
|
263
302
|
});
|
|
264
303
|
};
|
|
265
304
|
|
|
266
|
-
const hasToolbar = Boolean(search || toolbarStart || toolbarEnd);
|
|
305
|
+
const hasToolbar = Boolean(search || filters?.length || toolbarStart || toolbarEnd);
|
|
267
306
|
|
|
268
307
|
// Fixed layout keeps header and body columns aligned across the two tables.
|
|
269
308
|
// When no column declares a width, give the first column more room and split
|
|
@@ -317,49 +356,170 @@ export function DataTable<T>({
|
|
|
317
356
|
<div className="flex flex-wrap items-center gap-2">
|
|
318
357
|
{search && (
|
|
319
358
|
<Input
|
|
320
|
-
size="
|
|
359
|
+
size="sm"
|
|
321
360
|
type="search"
|
|
322
361
|
aria-label={search.placeholder ?? "Search"}
|
|
323
|
-
placeholder={search.placeholder ?? "Search
|
|
362
|
+
placeholder={search.placeholder ?? "Search..."}
|
|
324
363
|
value={search.value}
|
|
325
364
|
onChange={(e) => search.onChange(e.target.value)}
|
|
365
|
+
onClear={() => search.onChange("")}
|
|
326
366
|
leadingIcon={<MagnifyingGlass />}
|
|
327
|
-
containerClassName="w-full sm:w-
|
|
367
|
+
containerClassName="w-full sm:w-[280px]"
|
|
368
|
+
className="rounded-[6px] border-0 bg-surface shadow-xs ring-1 ring-line"
|
|
328
369
|
/>
|
|
329
370
|
)}
|
|
330
371
|
{toolbarStart}
|
|
331
|
-
<
|
|
372
|
+
<DropdownMenu>
|
|
373
|
+
<DropdownMenuTrigger asChild>
|
|
374
|
+
<Button
|
|
375
|
+
variant="secondary"
|
|
376
|
+
size="sm"
|
|
377
|
+
leadingIcon={<ColumnsIcon />}
|
|
378
|
+
trailingIcon={<CaretDown />}
|
|
379
|
+
className={TOOLBAR_BUTTON}
|
|
380
|
+
>
|
|
381
|
+
Columns
|
|
382
|
+
</Button>
|
|
383
|
+
</DropdownMenuTrigger>
|
|
384
|
+
<DropdownMenuContent align="end" className="w-54">
|
|
385
|
+
<DropdownMenuLabel className="text-[13px]">Active columns</DropdownMenuLabel>
|
|
386
|
+
{visibleColumns.map((c) => (
|
|
387
|
+
<DropdownMenuCheckboxItem
|
|
388
|
+
key={c.id}
|
|
389
|
+
checked
|
|
390
|
+
disabled={c.required}
|
|
391
|
+
onCheckedChange={() => setHidden((prev) => ({ ...prev, [c.id]: true }))}
|
|
392
|
+
onSelect={(e) => e.preventDefault()}
|
|
393
|
+
className="justify-between"
|
|
394
|
+
>
|
|
395
|
+
<span>{c.header}</span>
|
|
396
|
+
{c.required ? <Lock className="size-3.5 text-subtle" aria-hidden /> : null}
|
|
397
|
+
</DropdownMenuCheckboxItem>
|
|
398
|
+
))}
|
|
399
|
+
{hiddenColumns.length > 0 ? (
|
|
400
|
+
<>
|
|
401
|
+
<DropdownMenuSeparator />
|
|
402
|
+
<DropdownMenuLabel className="text-[13px]">Available columns</DropdownMenuLabel>
|
|
403
|
+
{hiddenColumns.map((c) => (
|
|
404
|
+
<DropdownMenuCheckboxItem
|
|
405
|
+
key={c.id}
|
|
406
|
+
checked={false}
|
|
407
|
+
onCheckedChange={() => setHidden((prev) => ({ ...prev, [c.id]: false }))}
|
|
408
|
+
onSelect={(e) => e.preventDefault()}
|
|
409
|
+
>
|
|
410
|
+
{c.header}
|
|
411
|
+
</DropdownMenuCheckboxItem>
|
|
412
|
+
))}
|
|
413
|
+
</>
|
|
414
|
+
) : null}
|
|
415
|
+
</DropdownMenuContent>
|
|
416
|
+
</DropdownMenu>
|
|
417
|
+
{filters && filters.length > 0 ? (
|
|
332
418
|
<DropdownMenu>
|
|
333
419
|
<DropdownMenuTrigger asChild>
|
|
334
420
|
<Button
|
|
335
421
|
variant="secondary"
|
|
336
|
-
size="
|
|
337
|
-
|
|
338
|
-
|
|
422
|
+
size="sm"
|
|
423
|
+
iconOnly
|
|
424
|
+
aria-label="Add filter"
|
|
425
|
+
className={TOOLBAR_BUTTON}
|
|
339
426
|
>
|
|
340
|
-
|
|
427
|
+
<FunnelSimple />
|
|
341
428
|
</Button>
|
|
342
429
|
</DropdownMenuTrigger>
|
|
343
|
-
<DropdownMenuContent align="
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
430
|
+
<DropdownMenuContent align="start" className="w-54">
|
|
431
|
+
{filters.map((filter) => (
|
|
432
|
+
<DropdownMenuItem
|
|
433
|
+
key={filter.id}
|
|
434
|
+
onSelect={() =>
|
|
435
|
+
setActiveFilterIds((prev) =>
|
|
436
|
+
prev.includes(filter.id) ? prev : [...prev, filter.id],
|
|
437
|
+
)
|
|
438
|
+
}
|
|
352
439
|
>
|
|
353
|
-
{
|
|
354
|
-
</
|
|
440
|
+
{filter.label}
|
|
441
|
+
</DropdownMenuItem>
|
|
355
442
|
))}
|
|
356
443
|
</DropdownMenuContent>
|
|
357
444
|
</DropdownMenu>
|
|
358
|
-
|
|
359
|
-
|
|
445
|
+
) : null}
|
|
446
|
+
{toolbarEnd ? (
|
|
447
|
+
<div className="flex-0 sm:ml-auto [&_button]:h-8 [&_button]:rounded-[6px]">
|
|
448
|
+
{toolbarEnd}
|
|
449
|
+
</div>
|
|
450
|
+
) : null}
|
|
360
451
|
</div>
|
|
361
452
|
)}
|
|
362
453
|
|
|
454
|
+
{activeFilters.length > 0 ? (
|
|
455
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
456
|
+
{activeFilters.map((filter) => (
|
|
457
|
+
<DropdownMenu key={filter.id}>
|
|
458
|
+
<DropdownMenuTrigger asChild>
|
|
459
|
+
<Button
|
|
460
|
+
variant="outline"
|
|
461
|
+
size="sm"
|
|
462
|
+
className="rounded-[6px] bg-surface shadow-xs"
|
|
463
|
+
>
|
|
464
|
+
<FunnelSimple />
|
|
465
|
+
<span className="font-medium">{filter.label}</span>
|
|
466
|
+
<span className="h-4 w-px bg-line" aria-hidden />
|
|
467
|
+
<span className="text-muted">
|
|
468
|
+
{filter.value.length > 0
|
|
469
|
+
? `${filter.value.length} selected`
|
|
470
|
+
: "Enter value"}
|
|
471
|
+
</span>
|
|
472
|
+
<CaretDown className="text-muted" />
|
|
473
|
+
</Button>
|
|
474
|
+
</DropdownMenuTrigger>
|
|
475
|
+
<DropdownMenuContent align="start" className="w-56">
|
|
476
|
+
<DropdownMenuLabel className="text-[13px]">
|
|
477
|
+
Filter by {filter.label}
|
|
478
|
+
</DropdownMenuLabel>
|
|
479
|
+
{filter.options.map((option) => {
|
|
480
|
+
const checked = filter.value.includes(option.value);
|
|
481
|
+
return (
|
|
482
|
+
<DropdownMenuCheckboxItem
|
|
483
|
+
key={option.value}
|
|
484
|
+
checked={checked}
|
|
485
|
+
onCheckedChange={() =>
|
|
486
|
+
filter.onChange(
|
|
487
|
+
checked
|
|
488
|
+
? filter.value.filter((value) => value !== option.value)
|
|
489
|
+
: [...filter.value, option.value],
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
onSelect={(e) => e.preventDefault()}
|
|
493
|
+
>
|
|
494
|
+
{option.label}
|
|
495
|
+
</DropdownMenuCheckboxItem>
|
|
496
|
+
);
|
|
497
|
+
})}
|
|
498
|
+
<DropdownMenuSeparator />
|
|
499
|
+
<DropdownMenuItem
|
|
500
|
+
onSelect={() => {
|
|
501
|
+
filter.onChange([]);
|
|
502
|
+
setActiveFilterIds((prev) => prev.filter((id) => id !== filter.id));
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
Clear filter
|
|
506
|
+
</DropdownMenuItem>
|
|
507
|
+
</DropdownMenuContent>
|
|
508
|
+
</DropdownMenu>
|
|
509
|
+
))}
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
onClick={() => {
|
|
513
|
+
for (const filter of activeFilters) filter.onChange([]);
|
|
514
|
+
setActiveFilterIds([]);
|
|
515
|
+
}}
|
|
516
|
+
className="text-sm text-muted transition-colors hover:text-fg"
|
|
517
|
+
>
|
|
518
|
+
Clear filters
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
) : null}
|
|
522
|
+
|
|
363
523
|
{infoBanner}
|
|
364
524
|
|
|
365
525
|
{selectable && selected.size > 0 && (
|
|
@@ -421,12 +581,16 @@ export function DataTable<T>({
|
|
|
421
581
|
className={cn(HEADER_CELL, c.align === "right" && "text-right", c.className)}
|
|
422
582
|
>
|
|
423
583
|
{c.sortable ? (
|
|
424
|
-
<
|
|
584
|
+
<Button
|
|
425
585
|
type="button"
|
|
586
|
+
variant="ghost"
|
|
587
|
+
size="xs"
|
|
426
588
|
aria-label={`Sort by ${c.header}`}
|
|
427
589
|
onClick={() => toggleSort(c)}
|
|
428
590
|
className={cn(
|
|
429
|
-
"
|
|
591
|
+
"h-auto gap-1.5 rounded-none px-0 py-0 text-[0.8125rem] text-table-header transition-colors [&_svg]:size-3.5",
|
|
592
|
+
"hover:bg-transparent hover:text-table-header-active hover:shadow-none",
|
|
593
|
+
"active:scale-100 active:bg-transparent active:shadow-none",
|
|
430
594
|
sort?.id === c.id && "text-table-header-active",
|
|
431
595
|
)}
|
|
432
596
|
>
|
|
@@ -440,7 +604,7 @@ export function DataTable<T>({
|
|
|
440
604
|
) : (
|
|
441
605
|
<CaretUpDown className="size-3.5 opacity-50" />
|
|
442
606
|
)}
|
|
443
|
-
</
|
|
607
|
+
</Button>
|
|
444
608
|
) : (
|
|
445
609
|
c.header
|
|
446
610
|
)}
|
package/src/input.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { X } from "@phosphor-icons/react";
|
|
2
|
+
import { forwardRef, useId, type InputHTMLAttributes, type ReactNode } from "react";
|
|
2
3
|
|
|
3
4
|
import { cn } from "./utils";
|
|
4
5
|
|
|
@@ -47,6 +48,8 @@ export interface InputProps
|
|
|
47
48
|
leadingIcon?: ReactNode;
|
|
48
49
|
/** Optional icon rendered inside the field, after the text. */
|
|
49
50
|
trailingIcon?: ReactNode;
|
|
51
|
+
/** Shows a real clear button when the field has a value. */
|
|
52
|
+
onClear?: () => void;
|
|
50
53
|
/** Error styling; also pairs with `helperText` for the message. */
|
|
51
54
|
error?: boolean;
|
|
52
55
|
/** Helper or error message shown below the field (wired to aria-describedby). */
|
|
@@ -64,6 +67,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
64
67
|
size = "md",
|
|
65
68
|
leadingIcon,
|
|
66
69
|
trailingIcon,
|
|
70
|
+
onClear,
|
|
71
|
+
value,
|
|
72
|
+
defaultValue,
|
|
67
73
|
error = false,
|
|
68
74
|
helperText,
|
|
69
75
|
id,
|
|
@@ -77,6 +83,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
77
83
|
const autoId = useId();
|
|
78
84
|
const inputId = id ?? autoId;
|
|
79
85
|
const hasMessage = Boolean(helperText);
|
|
86
|
+
const showClear = Boolean(onClear && (value !== undefined ? String(value).length > 0 : defaultValue !== undefined ? String(defaultValue).length > 0 : false));
|
|
80
87
|
const messageId = hasMessage ? `${inputId}-description` : undefined;
|
|
81
88
|
const describedBy = [ariaDescribedBy, messageId].filter(Boolean).join(" ") || undefined;
|
|
82
89
|
|
|
@@ -90,14 +97,15 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
90
97
|
aria-invalid={error || undefined}
|
|
91
98
|
aria-describedby={describedBy}
|
|
92
99
|
style={
|
|
93
|
-
leadingIcon || trailingIcon
|
|
100
|
+
leadingIcon || trailingIcon || showClear
|
|
94
101
|
? {
|
|
95
102
|
paddingInlineStart: leadingIcon
|
|
96
103
|
? `calc(var(--control-pad-x-${size}) + var(--control-icon-${size}) + var(--control-gap))`
|
|
97
104
|
: undefined,
|
|
98
|
-
paddingInlineEnd:
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
paddingInlineEnd:
|
|
106
|
+
trailingIcon || showClear
|
|
107
|
+
? `calc(var(--control-pad-x-${size}) + var(--control-icon-${size}) + var(--control-gap))`
|
|
108
|
+
: undefined,
|
|
101
109
|
}
|
|
102
110
|
: undefined
|
|
103
111
|
}
|
|
@@ -116,12 +124,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
116
124
|
readOnly && "cursor-default focus-visible:ring-0",
|
|
117
125
|
className,
|
|
118
126
|
)}
|
|
127
|
+
value={value}
|
|
128
|
+
defaultValue={defaultValue}
|
|
119
129
|
{...props}
|
|
120
130
|
/>
|
|
121
131
|
);
|
|
122
132
|
|
|
123
133
|
// Bare input keeps the historical API when no compositional features are used.
|
|
124
|
-
if (!leadingIcon && !trailingIcon && !hasMessage) {
|
|
134
|
+
if (!leadingIcon && !trailingIcon && !showClear && !hasMessage) {
|
|
125
135
|
return field;
|
|
126
136
|
}
|
|
127
137
|
|
|
@@ -142,10 +152,24 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
142
152
|
</span>
|
|
143
153
|
) : null}
|
|
144
154
|
{field}
|
|
145
|
-
{
|
|
155
|
+
{showClear ? (
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
aria-label="Clear search"
|
|
159
|
+
onClick={onClear}
|
|
160
|
+
className={cn(
|
|
161
|
+
"absolute inset-y-0 flex items-center text-[var(--input-placeholder)] transition-colors hover:text-[var(--input-fg)]",
|
|
162
|
+
TRAIL_INSET[size],
|
|
163
|
+
)}
|
|
164
|
+
>
|
|
165
|
+
<span className={cn("inline-flex items-center justify-center", ICON[size])}>
|
|
166
|
+
<X />
|
|
167
|
+
</span>
|
|
168
|
+
</button>
|
|
169
|
+
) : trailingIcon ? (
|
|
146
170
|
<span
|
|
147
171
|
className={cn(
|
|
148
|
-
"absolute inset-y-0 flex items-center text-[var(--input-placeholder)]",
|
|
172
|
+
"pointer-events-none absolute inset-y-0 flex items-center text-[var(--input-placeholder)]",
|
|
149
173
|
TRAIL_INSET[size],
|
|
150
174
|
)}
|
|
151
175
|
aria-hidden
|