aplance-ui 1.2.1 → 1.2.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/package.json +1 -1
- package/registry/components/primitives/modal/modal.tsx +46 -13
- package/registry/components/primitives/popover/popover.tsx +6 -0
- package/registry/components/ui/data-table/data-table.module.css +89 -20
- package/registry/components/ui/data-table/data-table.tsx +193 -111
- package/registry/components/ui/data-table/pagination-bar.tsx +174 -71
- package/registry/components/ui/data-table/types.ts +12 -3
- package/registry/components/ui/input-fields/common/input-field-native.module.css +52 -42
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +76 -10
- package/registry/components/ui/input-fields/input/input.tsx +69 -51
- package/registry/components/ui/input-fields/media-input/media-input.module.css +29 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +25 -7
- package/registry/hooks/useFocusOutside.tsx +21 -6
- package/registry/registry.json +2 -1
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
type Updater,
|
|
13
13
|
} from "@tanstack/react-table";
|
|
14
14
|
import {
|
|
15
|
-
useCallback,
|
|
16
|
-
useEffect,
|
|
17
|
-
useLayoutEffect,
|
|
18
|
-
useMemo,
|
|
19
|
-
useRef,
|
|
20
|
-
useState,
|
|
15
|
+
useCallback,
|
|
16
|
+
useEffect,
|
|
17
|
+
useLayoutEffect,
|
|
18
|
+
useMemo,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
21
|
type CSSProperties,
|
|
22
22
|
type DragEvent as ReactDragEvent,
|
|
23
23
|
type ReactNode,
|
|
@@ -74,13 +74,15 @@ const EMPTY_FILTER_VALUES: Record<string, string> = {};
|
|
|
74
74
|
const ICON_SIZE = 15;
|
|
75
75
|
const SORT_ICON_SIZE = 13;
|
|
76
76
|
const ICON_STROKE = 1.6;
|
|
77
|
-
const EMPTY_COLUMN_EXTRA_WIDTHS: Record<string, number> = {};
|
|
78
|
-
// Grace period after `isLoading` flips false before committing to the empty
|
|
79
|
-
// state. Eliminates the flash when consumers set isLoading=false a render
|
|
80
|
-
// before populating rows, without making genuinely-empty results feel sluggish.
|
|
81
|
-
const EMPTY_STATE_GRACE_MS = 150;
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
const EMPTY_COLUMN_EXTRA_WIDTHS: Record<string, number> = {};
|
|
78
|
+
// Grace period after `isLoading` flips false before committing to the empty
|
|
79
|
+
// state. Eliminates the flash when consumers set isLoading=false a render
|
|
80
|
+
// before populating rows, without making genuinely-empty results feel sluggish.
|
|
81
|
+
const EMPTY_STATE_GRACE_MS = 150;
|
|
82
|
+
|
|
83
|
+
const MIN_SKELETON_ROWS = 1;
|
|
84
|
+
const useIsomorphicLayoutEffect =
|
|
85
|
+
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
84
86
|
|
|
85
87
|
export const DataTable = <TData extends { id: string }>({
|
|
86
88
|
data,
|
|
@@ -106,6 +108,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
106
108
|
rowLabel = "row",
|
|
107
109
|
|
|
108
110
|
rowsSelectable = false,
|
|
111
|
+
getRowCanSelect,
|
|
109
112
|
rowSelection = {},
|
|
110
113
|
onRowSelectionChange,
|
|
111
114
|
|
|
@@ -220,7 +223,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
220
223
|
const table = useReactTable<TData>({
|
|
221
224
|
data,
|
|
222
225
|
columns,
|
|
223
|
-
enableRowSelection: rowsSelectable,
|
|
226
|
+
enableRowSelection: rowsSelectable ? (getRowCanSelect ?? true) : false,
|
|
224
227
|
enableColumnResizing,
|
|
225
228
|
enableColumnPinning,
|
|
226
229
|
enableGrouping,
|
|
@@ -342,9 +345,11 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
342
345
|
// ── Sync header/body scrolling ──────────────────────────────────────────────
|
|
343
346
|
// The header lives in its own `overflow:hidden` container; we drive its
|
|
344
347
|
// scrollLeft from the body's scrollLeft on every scroll event so pinned
|
|
345
|
-
// columns stay perfectly aligned with their body counterparts.
|
|
346
|
-
const headerRef = useRef<HTMLDivElement>(null);
|
|
347
|
-
const bodyRef = useRef<HTMLDivElement>(null);
|
|
348
|
+
// columns stay perfectly aligned with their body counterparts.
|
|
349
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
350
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
351
|
+
const bodyTableRef = useRef<HTMLTableElement>(null);
|
|
352
|
+
const skeletonRowRef = useRef<HTMLTableRowElement>(null);
|
|
348
353
|
const rows = table.getRowModel().rows;
|
|
349
354
|
const headerCount = table.getFlatHeaders().length;
|
|
350
355
|
const totalWidth = table.getTotalSize();
|
|
@@ -352,19 +357,38 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
352
357
|
const tableColumnPinning = tableState.columnPinning;
|
|
353
358
|
const tableColumnSizing = tableState.columnSizing;
|
|
354
359
|
const tableColumnOrder = tableState.columnOrder;
|
|
355
|
-
const tableColumnVisibility = tableState.columnVisibility;
|
|
356
|
-
const tableGrouping = tableState.grouping;
|
|
357
|
-
const [bodyViewportWidth, setBodyViewportWidth] = useState(0);
|
|
358
|
-
const bodyViewportWidthRef = useRef(0);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
360
|
+
const tableColumnVisibility = tableState.columnVisibility;
|
|
361
|
+
const tableGrouping = tableState.grouping;
|
|
362
|
+
const [bodyViewportWidth, setBodyViewportWidth] = useState(0);
|
|
363
|
+
const bodyViewportWidthRef = useRef(0);
|
|
364
|
+
// Body wrap height tracked separately so the loading skeleton can render
|
|
365
|
+
// exactly the count of rows that fits the visible body. Avoids the
|
|
366
|
+
// "skeleton overflows + body wrap scrolls" feel during cold load.
|
|
367
|
+
const [bodyViewportHeight, setBodyViewportHeight] = useState(0);
|
|
368
|
+
const bodyViewportHeightRef = useRef(0);
|
|
369
|
+
const [skeletonRowHeight, setSkeletonRowHeight] = useState(0);
|
|
370
|
+
const skeletonRowHeightRef = useRef(0);
|
|
371
|
+
|
|
372
|
+
const commitBodyViewportWidth = useCallback((nextWidth: number) => {
|
|
373
|
+
if (bodyViewportWidthRef.current === nextWidth) return;
|
|
374
|
+
bodyViewportWidthRef.current = nextWidth;
|
|
375
|
+
setBodyViewportWidth(nextWidth);
|
|
376
|
+
}, []);
|
|
377
|
+
|
|
378
|
+
const commitBodyViewportHeight = useCallback((nextHeight: number) => {
|
|
379
|
+
if (bodyViewportHeightRef.current === nextHeight) return;
|
|
380
|
+
bodyViewportHeightRef.current = nextHeight;
|
|
381
|
+
setBodyViewportHeight(nextHeight);
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
384
|
+
const commitSkeletonRowHeight = useCallback((nextHeight: number) => {
|
|
385
|
+
if (nextHeight <= 0 || skeletonRowHeightRef.current === nextHeight) return;
|
|
386
|
+
skeletonRowHeightRef.current = nextHeight;
|
|
387
|
+
setSkeletonRowHeight(nextHeight);
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
const syncHeaderMetrics = useCallback(() => {
|
|
391
|
+
const body = bodyRef.current;
|
|
368
392
|
const header = headerRef.current;
|
|
369
393
|
if (!body || !header) return;
|
|
370
394
|
|
|
@@ -379,38 +403,40 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
379
403
|
);
|
|
380
404
|
header.parentElement?.setAttribute("data-has-scroll", hasScroll.toString());
|
|
381
405
|
|
|
382
|
-
// Re-sync horizontal scroll position after width changes so pinned
|
|
383
|
-
// columns stay perfectly aligned with their body counterparts.
|
|
384
|
-
header.scrollLeft = body.scrollLeft;
|
|
385
|
-
commitBodyViewportWidth(body.clientWidth);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
406
|
+
// Re-sync horizontal scroll position after width changes so pinned
|
|
407
|
+
// columns stay perfectly aligned with their body counterparts.
|
|
408
|
+
header.scrollLeft = body.scrollLeft;
|
|
409
|
+
commitBodyViewportWidth(body.clientWidth);
|
|
410
|
+
commitBodyViewportHeight(body.clientHeight);
|
|
411
|
+
}, [commitBodyViewportWidth, commitBodyViewportHeight]);
|
|
412
|
+
|
|
413
|
+
useIsomorphicLayoutEffect(() => {
|
|
414
|
+
syncHeaderMetrics();
|
|
415
|
+
}, [
|
|
416
|
+
rows.length,
|
|
392
417
|
headerCount,
|
|
393
418
|
totalWidth,
|
|
394
419
|
tableColumnPinning,
|
|
395
420
|
tableColumnSizing,
|
|
396
421
|
tableColumnOrder,
|
|
397
422
|
tableColumnVisibility,
|
|
398
|
-
tableGrouping,
|
|
399
|
-
syncHeaderMetrics,
|
|
400
|
-
]);
|
|
401
|
-
|
|
402
|
-
useIsomorphicLayoutEffect(() => {
|
|
403
|
-
const body = bodyRef.current;
|
|
404
|
-
if (!body) return;
|
|
405
|
-
if (typeof ResizeObserver === "undefined") return;
|
|
406
|
-
|
|
407
|
-
const observer = new ResizeObserver(syncHeaderMetrics);
|
|
408
|
-
observer.observe(body);
|
|
423
|
+
tableGrouping,
|
|
424
|
+
syncHeaderMetrics,
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
useIsomorphicLayoutEffect(() => {
|
|
428
|
+
const body = bodyRef.current;
|
|
429
|
+
if (!body) return;
|
|
430
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
431
|
+
|
|
432
|
+
const observer = new ResizeObserver(syncHeaderMetrics);
|
|
433
|
+
observer.observe(body);
|
|
434
|
+
if (bodyTableRef.current) observer.observe(bodyTableRef.current);
|
|
409
435
|
|
|
410
436
|
return () => {
|
|
411
437
|
observer.disconnect();
|
|
412
438
|
};
|
|
413
|
-
}, [syncHeaderMetrics]);
|
|
439
|
+
}, [syncHeaderMetrics, rows.length, isLoading]);
|
|
414
440
|
|
|
415
441
|
useEffect(() => {
|
|
416
442
|
const body = bodyRef.current;
|
|
@@ -439,7 +465,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
439
465
|
}, []);
|
|
440
466
|
|
|
441
467
|
// ── Derived UI state ────────────────────────────────────────────────────────
|
|
442
|
-
// Defer the empty state by a tick after a loading
|
|
468
|
+
// Defer the empty state by a tick after a loading-to-idle transition so the
|
|
443
469
|
// loader doesn't flicker into "no results" for one paint before rows land.
|
|
444
470
|
// Genuinely empty results (no in-flight load) commit immediately on mount.
|
|
445
471
|
const isRowsEmpty = rows.length === 0;
|
|
@@ -467,7 +493,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
467
493
|
}, [isLoading, isRowsEmpty]);
|
|
468
494
|
|
|
469
495
|
// Single tab-like discriminator drives the body - loader, empty, or content.
|
|
470
|
-
// While loading OR while bridging the loading
|
|
496
|
+
// While loading OR while bridging the loading-to-empty grace window, the
|
|
471
497
|
// loader stays visible to avoid the brief "no results" flash.
|
|
472
498
|
const view: "loader" | "empty" | "content" =
|
|
473
499
|
isLoading || (isRowsEmpty && !showEmptyState)
|
|
@@ -476,15 +502,43 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
476
502
|
? "empty"
|
|
477
503
|
: "content";
|
|
478
504
|
|
|
505
|
+
useIsomorphicLayoutEffect(() => {
|
|
506
|
+
if (view !== "loader") return;
|
|
507
|
+
const row = skeletonRowRef.current;
|
|
508
|
+
if (!row) return;
|
|
509
|
+
|
|
510
|
+
const measureRow = () => {
|
|
511
|
+
commitSkeletonRowHeight(Math.ceil(row.getBoundingClientRect().height));
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
measureRow();
|
|
515
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
516
|
+
|
|
517
|
+
const observer = new ResizeObserver(measureRow);
|
|
518
|
+
observer.observe(row);
|
|
519
|
+
|
|
520
|
+
return () => {
|
|
521
|
+
observer.disconnect();
|
|
522
|
+
};
|
|
523
|
+
}, [commitSkeletonRowHeight, view]);
|
|
524
|
+
|
|
479
525
|
const selectedCount = rowsSelectable
|
|
480
526
|
? table.getSelectedRowModel().flatRows.length
|
|
481
527
|
: 0;
|
|
482
|
-
const headerGroups = table.getHeaderGroups();
|
|
483
|
-
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
528
|
+
const headerGroups = table.getHeaderGroups();
|
|
529
|
+
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
530
|
+
|
|
531
|
+
const skeletonRowCount =
|
|
532
|
+
bodyViewportHeight > 0 && skeletonRowHeight > 0
|
|
533
|
+
? Math.max(
|
|
534
|
+
MIN_SKELETON_ROWS,
|
|
535
|
+
Math.floor(bodyViewportHeight / skeletonRowHeight),
|
|
536
|
+
)
|
|
537
|
+
: MIN_SKELETON_ROWS;
|
|
538
|
+
|
|
539
|
+
const columnExtraWidths = useMemo<Record<string, number>>(() => {
|
|
540
|
+
const extraSpace = Math.max(0, bodyViewportWidth - totalWidth);
|
|
541
|
+
if (extraSpace <= 0) return EMPTY_COLUMN_EXTRA_WIDTHS;
|
|
488
542
|
|
|
489
543
|
// Never add responsive width onto pinned columns because tanstack computes
|
|
490
544
|
// sticky offsets from base sizes; changing pinned visual width would desync
|
|
@@ -507,17 +561,27 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
507
561
|
remainder -= 1;
|
|
508
562
|
}
|
|
509
563
|
}
|
|
510
|
-
|
|
511
|
-
return next;
|
|
512
|
-
}, [bodyViewportWidth, totalWidth, visibleLeafColumns]);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
564
|
+
|
|
565
|
+
return next;
|
|
566
|
+
}, [bodyViewportWidth, totalWidth, visibleLeafColumns]);
|
|
567
|
+
|
|
568
|
+
const tableWidth = Math.max(totalWidth, bodyViewportWidth);
|
|
569
|
+
|
|
570
|
+
// Match the table element width to the measured column widths. This keeps
|
|
571
|
+
// header, body, and skeleton rows on the same sizing contract during resize.
|
|
572
|
+
const tableSizingStyle: CSSProperties = useMemo(
|
|
573
|
+
() => ({ width: `${tableWidth}px`, minWidth: `${totalWidth}px` }),
|
|
574
|
+
[tableWidth, totalWidth],
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const bodySkeletonStyle: CSSProperties = {
|
|
578
|
+
height: "var(--spacing-9)",
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const selectSkeletonStyle: CSSProperties = {
|
|
582
|
+
width: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
583
|
+
height: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
584
|
+
};
|
|
521
585
|
|
|
522
586
|
const tableClass = clsx(
|
|
523
587
|
s.table,
|
|
@@ -580,60 +644,77 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
580
644
|
{/* Scrollable body - only this element scrolls. The body is a
|
|
581
645
|
single switch over `view`: at most one branch ever renders, the
|
|
582
646
|
other two are unmounted entirely. */}
|
|
583
|
-
<div
|
|
584
|
-
className={s.table_body_wrap}
|
|
585
|
-
style={{ pointerEvents: isLoading ? "none" : undefined }}
|
|
586
|
-
ref={bodyRef}
|
|
587
|
-
>
|
|
588
|
-
{view === "loader" && (
|
|
589
|
-
<table className={tableClass} style={tableSizingStyle}>
|
|
590
|
-
<tbody>
|
|
591
|
-
{Array.from({ length:
|
|
592
|
-
(_,
|
|
593
|
-
<tr
|
|
594
|
-
{
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return (
|
|
605
|
-
<td
|
|
606
|
-
key={
|
|
607
|
-
className={clsx(
|
|
608
|
-
isSelectColumn && s.col_select,
|
|
609
|
-
isPinned === "left" && s.col_pinned_left,
|
|
610
|
-
isPinned === "right" && s.col_pinned_right,
|
|
611
|
-
isLastPinnedCol && s.col_pinned_last,
|
|
612
|
-
)}
|
|
613
|
-
style={
|
|
647
|
+
<div
|
|
648
|
+
className={s.table_body_wrap}
|
|
649
|
+
style={{ pointerEvents: isLoading ? "none" : undefined }}
|
|
650
|
+
ref={bodyRef}
|
|
651
|
+
>
|
|
652
|
+
{view === "loader" && (
|
|
653
|
+
<table className={tableClass} style={tableSizingStyle}>
|
|
654
|
+
<tbody>
|
|
655
|
+
{Array.from({ length: skeletonRowCount }).map(
|
|
656
|
+
(_, rowIndex) => (
|
|
657
|
+
<tr
|
|
658
|
+
key={`skeleton-${rowIndex}`}
|
|
659
|
+
ref={rowIndex === 0 ? skeletonRowRef : undefined}
|
|
660
|
+
>
|
|
661
|
+
{visibleLeafColumns.map((column) => {
|
|
662
|
+
const isPinned = column.getIsPinned();
|
|
663
|
+
const isLastPinnedCol =
|
|
664
|
+
isPinned && isLastPinned(column);
|
|
665
|
+
const isSelectColumn =
|
|
666
|
+
column.id === SELECT_COLUMN_ID;
|
|
667
|
+
|
|
668
|
+
return (
|
|
669
|
+
<td
|
|
670
|
+
key={column.id}
|
|
671
|
+
className={clsx(
|
|
672
|
+
isSelectColumn && s.col_select,
|
|
673
|
+
isPinned === "left" && s.col_pinned_left,
|
|
674
|
+
isPinned === "right" && s.col_pinned_right,
|
|
675
|
+
isLastPinnedCol && s.col_pinned_last,
|
|
676
|
+
)}
|
|
677
|
+
style={getColumnStyle(
|
|
678
|
+
column,
|
|
679
|
+
column.getSize(),
|
|
680
|
+
columnExtraWidths[column.id] ?? 0,
|
|
681
|
+
)}
|
|
614
682
|
>
|
|
615
683
|
<div className={s.td_inner}>
|
|
616
684
|
<Loader
|
|
617
685
|
variant="skeleton"
|
|
618
|
-
|
|
686
|
+
className={
|
|
687
|
+
isSelectColumn
|
|
688
|
+
? s.loading_select_skeleton
|
|
689
|
+
: undefined
|
|
690
|
+
}
|
|
691
|
+
style={
|
|
692
|
+
isSelectColumn
|
|
693
|
+
? selectSkeletonStyle
|
|
694
|
+
: bodySkeletonStyle
|
|
695
|
+
}
|
|
619
696
|
/>
|
|
620
697
|
</div>
|
|
621
698
|
</td>
|
|
622
699
|
);
|
|
623
|
-
})}
|
|
624
|
-
</tr>
|
|
625
|
-
),
|
|
626
|
-
)}
|
|
627
|
-
</tbody>
|
|
628
|
-
</table>
|
|
629
|
-
)}
|
|
700
|
+
})}
|
|
701
|
+
</tr>
|
|
702
|
+
),
|
|
703
|
+
)}
|
|
704
|
+
</tbody>
|
|
705
|
+
</table>
|
|
706
|
+
)}
|
|
630
707
|
{view === "empty" && (
|
|
631
708
|
<div className={s.state_overlay}>
|
|
632
709
|
<p className={s.empty_msg}>{emptyMessage}</p>
|
|
633
710
|
</div>
|
|
634
711
|
)}
|
|
635
712
|
{view === "content" && (
|
|
636
|
-
<table
|
|
713
|
+
<table
|
|
714
|
+
className={tableClass}
|
|
715
|
+
style={tableSizingStyle}
|
|
716
|
+
ref={bodyTableRef}
|
|
717
|
+
>
|
|
637
718
|
<tbody>
|
|
638
719
|
{rows.map((row) => (
|
|
639
720
|
<BodyRow<TData>
|
|
@@ -666,6 +747,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
666
747
|
onPaginationChange={onPaginationChange}
|
|
667
748
|
pageSizeOptions={pageSizeOptions}
|
|
668
749
|
showPageSizeSelect={showPageSizeSelect}
|
|
750
|
+
isLoading={view === "loader"}
|
|
669
751
|
totalCount={totalCount}
|
|
670
752
|
rowLabel={rowLabel}
|
|
671
753
|
rowsSelectable={rowsSelectable}
|