aplance-ui 1.2.1 → 1.3.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/modal/modal.tsx +52 -13
- package/registry/components/primitives/popover/popover-context.ts +1 -0
- package/registry/components/primitives/popover/popover.tsx +13 -2
- package/registry/components/primitives/slider/slider.module.css +62 -5
- package/registry/components/primitives/slider/slider.tsx +192 -34
- package/registry/components/primitives/slider/types.ts +56 -3
- package/registry/components/primitives/slider/utils.ts +9 -0
- package/registry/components/ui/data-table/data-table.module.css +124 -6
- package/registry/components/ui/data-table/data-table.tsx +176 -88
- 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.module.css +21 -1
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +90 -9
- package/registry/components/ui/input-fields/input/input.tsx +58 -29
- 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/components/ui/tabs/tabs.module.css +22 -8
- package/registry/components/ui/tabs/tabs.tsx +5 -1
- package/registry/hooks/useFocusOutside.tsx +21 -6
- package/registry/hooks/useHasMounted.ts +22 -0
- package/registry/registry.json +21 -3
|
@@ -29,6 +29,18 @@
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* On tablet/phone the configured multi-column row can't fit. Stack every field
|
|
33
|
+
full width instead of overflowing the page or scrolling (a scroll container
|
|
34
|
+
would clip the inputs' focus rings). `!important` beats the inline
|
|
35
|
+
grid-template-columns the FilterBar sets per consumer; `stretch` makes each
|
|
36
|
+
field fill the row. */
|
|
37
|
+
@media (--md) {
|
|
38
|
+
.filter_bar {
|
|
39
|
+
grid-template-columns: 1fr !important;
|
|
40
|
+
justify-items: stretch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/* ─────────────────────────────────────────────
|
|
33
45
|
Content wrap
|
|
34
46
|
Grows to fill all space below the filter bar.
|
|
@@ -113,17 +125,32 @@
|
|
|
113
125
|
min-height: 0;
|
|
114
126
|
overflow-x: auto;
|
|
115
127
|
overflow-y: auto;
|
|
128
|
+
/* Reserve the scrollbar gutter by DEFAULT so clientWidth is stable: the
|
|
129
|
+
vertical scrollbar (when rows overflow) sits in the reserved gutter instead
|
|
130
|
+
of stealing ~15px and triggering an unexpected horizontal scrollbar. This
|
|
131
|
+
default also covers the first paint before JS measures. */
|
|
132
|
+
scrollbar-gutter: stable;
|
|
116
133
|
border: var(--border-1) solid var(--color-border);
|
|
117
|
-
border-radius: var(--radius-md);
|
|
118
|
-
background: var(--color-background);
|
|
119
|
-
position: relative;
|
|
120
|
-
}
|
|
134
|
+
border-radius: var(--radius-md);
|
|
135
|
+
background: var(--color-background);
|
|
136
|
+
position: relative;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Once JS confirms the body does NOT overflow vertically (no scrollbar to make
|
|
140
|
+
room for), release the gutter so the content fills to the border - otherwise
|
|
141
|
+
a few-row table shows a blank ~15px strip on the right. `data-has-scroll` is
|
|
142
|
+
set from `scrollHeight > clientHeight` in syncHeaderMetrics. */
|
|
143
|
+
.table_body_wrap[data-has-scroll="false"] {
|
|
144
|
+
scrollbar-gutter: auto;
|
|
145
|
+
}
|
|
121
146
|
|
|
122
147
|
/* ─────────────────────────────────────────────
|
|
123
148
|
Shared <table> - header wrap + body wrap.
|
|
124
149
|
table-layout:fixed + identical widths = perfect column alignment.
|
|
125
150
|
───────────────────────────────────────────── */
|
|
126
151
|
.table {
|
|
152
|
+
--data-table-selection-control-size: var(--spacing-9);
|
|
153
|
+
|
|
127
154
|
border-collapse: separate;
|
|
128
155
|
border-spacing: 0;
|
|
129
156
|
table-layout: fixed;
|
|
@@ -295,6 +322,8 @@
|
|
|
295
322
|
}
|
|
296
323
|
|
|
297
324
|
.selection_checkbox {
|
|
325
|
+
--native-control-size: var(--data-table-selection-control-size);
|
|
326
|
+
|
|
298
327
|
width: fit-content;
|
|
299
328
|
min-width: fit-content;
|
|
300
329
|
display: inline-flex;
|
|
@@ -532,6 +561,36 @@
|
|
|
532
561
|
justify-content: space-between;
|
|
533
562
|
gap: var(--spacing-8);
|
|
534
563
|
min-width: 0;
|
|
564
|
+
/* When the count + controls can't fit, the WHOLE bar scrolls horizontally as
|
|
565
|
+
one strip rather than wrapping or clipping. The outlet hides X overflow, so
|
|
566
|
+
letting a cluster shrink would just clip the nav buttons - scroll the bar
|
|
567
|
+
instead. `space-between` still spreads them when there IS room. The vertical
|
|
568
|
+
padding gives the buttons' focus rings room so `overflow-x: auto` (which also
|
|
569
|
+
clips the Y axis) doesn't crop them. */
|
|
570
|
+
overflow-x: auto;
|
|
571
|
+
scrollbar-width: thin;
|
|
572
|
+
scrollbar-color: var(--color-border) transparent;
|
|
573
|
+
padding-block: var(--spacing-2);
|
|
574
|
+
|
|
575
|
+
> * {
|
|
576
|
+
flex-shrink: 0;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@media (--sm) {
|
|
581
|
+
.pagination_bar {
|
|
582
|
+
gap: var(--spacing-5);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.pagination_bar_loading {
|
|
587
|
+
flex-wrap: wrap;
|
|
588
|
+
row-gap: var(--spacing-4);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.loading_select_skeleton {
|
|
592
|
+
border-radius: var(--radius-sm);
|
|
593
|
+
flex-shrink: 0;
|
|
535
594
|
}
|
|
536
595
|
|
|
537
596
|
.pagination_left {
|
|
@@ -539,7 +598,10 @@
|
|
|
539
598
|
align-items: center;
|
|
540
599
|
gap: var(--spacing-8);
|
|
541
600
|
min-width: 0;
|
|
542
|
-
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.pagination_left_loading {
|
|
604
|
+
overflow: hidden;
|
|
543
605
|
}
|
|
544
606
|
|
|
545
607
|
.pagination_count {
|
|
@@ -557,13 +619,59 @@
|
|
|
557
619
|
display: flex;
|
|
558
620
|
align-items: center;
|
|
559
621
|
gap: var(--spacing-8);
|
|
560
|
-
|
|
622
|
+
min-width: 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@media (--sm) {
|
|
626
|
+
.pagination_controls {
|
|
627
|
+
gap: var(--spacing-5);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.pagination_controls_loading {
|
|
632
|
+
flex-direction: row;
|
|
633
|
+
flex-wrap: wrap;
|
|
634
|
+
align-items: center;
|
|
635
|
+
justify-content: flex-end;
|
|
636
|
+
max-width: 100%;
|
|
637
|
+
row-gap: var(--spacing-4);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.pagination_controls_loading > * {
|
|
641
|
+
flex: 0 1 auto;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
max-width: 100%;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.pagination_text_skeleton {
|
|
647
|
+
height: 1em;
|
|
648
|
+
min-width: 0;
|
|
649
|
+
max-width: 100%;
|
|
650
|
+
flex: 0 1 auto;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.pagination_select_skeleton,
|
|
654
|
+
.pagination_button_skeleton {
|
|
655
|
+
width: var(--spacing-11);
|
|
656
|
+
height: var(--spacing-11);
|
|
657
|
+
min-width: 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.pagination_select_skeleton {
|
|
661
|
+
flex: 0 1 auto;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.pagination_button_skeleton {
|
|
665
|
+
flex: 0 0 auto;
|
|
561
666
|
}
|
|
562
667
|
|
|
563
668
|
.page_size_wrap {
|
|
564
669
|
display: flex;
|
|
565
670
|
align-items: center;
|
|
566
671
|
gap: var(--spacing-5);
|
|
672
|
+
min-width: 0;
|
|
673
|
+
flex-shrink: 1;
|
|
674
|
+
max-width: 100%;
|
|
567
675
|
}
|
|
568
676
|
|
|
569
677
|
.page_size_label {
|
|
@@ -589,4 +697,14 @@
|
|
|
589
697
|
display: flex;
|
|
590
698
|
align-items: center;
|
|
591
699
|
gap: var(--spacing-2);
|
|
700
|
+
flex-shrink: 0;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.page_nav_loading {
|
|
704
|
+
flex-wrap: wrap;
|
|
705
|
+
justify-content: flex-end;
|
|
706
|
+
row-gap: var(--spacing-2);
|
|
707
|
+
min-width: 0;
|
|
708
|
+
max-width: 100%;
|
|
709
|
+
flex-shrink: 1;
|
|
592
710
|
}
|
|
@@ -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,
|
|
@@ -345,6 +348,8 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
345
348
|
// columns stay perfectly aligned with their body counterparts.
|
|
346
349
|
const headerRef = useRef<HTMLDivElement>(null);
|
|
347
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
|
|
|
@@ -378,39 +402,44 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
378
402
|
paddingRight,
|
|
379
403
|
);
|
|
380
404
|
header.parentElement?.setAttribute("data-has-scroll", hasScroll.toString());
|
|
381
|
-
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}, [
|
|
391
|
-
|
|
405
|
+
// Mirror onto the body wrap so CSS can release the reserved scrollbar gutter
|
|
406
|
+
// when there's no vertical overflow (no blank strip on a few-row table).
|
|
407
|
+
body.setAttribute("data-has-scroll", hasScroll.toString());
|
|
408
|
+
|
|
409
|
+
// Re-sync horizontal scroll position after width changes so pinned
|
|
410
|
+
// columns stay perfectly aligned with their body counterparts.
|
|
411
|
+
header.scrollLeft = body.scrollLeft;
|
|
412
|
+
commitBodyViewportWidth(body.clientWidth);
|
|
413
|
+
commitBodyViewportHeight(body.clientHeight);
|
|
414
|
+
}, [commitBodyViewportWidth, commitBodyViewportHeight]);
|
|
415
|
+
|
|
416
|
+
useIsomorphicLayoutEffect(() => {
|
|
417
|
+
syncHeaderMetrics();
|
|
418
|
+
}, [
|
|
419
|
+
rows.length,
|
|
392
420
|
headerCount,
|
|
393
421
|
totalWidth,
|
|
394
422
|
tableColumnPinning,
|
|
395
423
|
tableColumnSizing,
|
|
396
424
|
tableColumnOrder,
|
|
397
425
|
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);
|
|
426
|
+
tableGrouping,
|
|
427
|
+
syncHeaderMetrics,
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
useIsomorphicLayoutEffect(() => {
|
|
431
|
+
const body = bodyRef.current;
|
|
432
|
+
if (!body) return;
|
|
433
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
434
|
+
|
|
435
|
+
const observer = new ResizeObserver(syncHeaderMetrics);
|
|
436
|
+
observer.observe(body);
|
|
437
|
+
if (bodyTableRef.current) observer.observe(bodyTableRef.current);
|
|
409
438
|
|
|
410
439
|
return () => {
|
|
411
440
|
observer.disconnect();
|
|
412
441
|
};
|
|
413
|
-
}, [syncHeaderMetrics]);
|
|
442
|
+
}, [syncHeaderMetrics, rows.length, isLoading]);
|
|
414
443
|
|
|
415
444
|
useEffect(() => {
|
|
416
445
|
const body = bodyRef.current;
|
|
@@ -439,7 +468,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
439
468
|
}, []);
|
|
440
469
|
|
|
441
470
|
// ── Derived UI state ────────────────────────────────────────────────────────
|
|
442
|
-
// Defer the empty state by a tick after a loading
|
|
471
|
+
// Defer the empty state by a tick after a loading-to-idle transition so the
|
|
443
472
|
// loader doesn't flicker into "no results" for one paint before rows land.
|
|
444
473
|
// Genuinely empty results (no in-flight load) commit immediately on mount.
|
|
445
474
|
const isRowsEmpty = rows.length === 0;
|
|
@@ -448,6 +477,9 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
448
477
|
);
|
|
449
478
|
const wasLoadingRef = useRef(isLoading);
|
|
450
479
|
|
|
480
|
+
// wasLoadingRef is intentionally omitted from the dep array: it's a ref used
|
|
481
|
+
// to track the previous value of isLoading across renders without triggering
|
|
482
|
+
// re-runs. React guarantees ref objects are stable across the component lifetime.
|
|
451
483
|
useEffect(() => {
|
|
452
484
|
const wasLoading = wasLoadingRef.current;
|
|
453
485
|
wasLoadingRef.current = isLoading;
|
|
@@ -456,18 +488,19 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
456
488
|
setShowEmptyState(false);
|
|
457
489
|
return;
|
|
458
490
|
}
|
|
459
|
-
if (wasLoading) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
EMPTY_STATE_GRACE_MS,
|
|
463
|
-
);
|
|
464
|
-
return () => window.clearTimeout(id);
|
|
491
|
+
if (!wasLoading) {
|
|
492
|
+
setShowEmptyState(true);
|
|
493
|
+
return;
|
|
465
494
|
}
|
|
466
|
-
|
|
495
|
+
const id = window.setTimeout(
|
|
496
|
+
() => setShowEmptyState(true),
|
|
497
|
+
EMPTY_STATE_GRACE_MS,
|
|
498
|
+
);
|
|
499
|
+
return () => window.clearTimeout(id);
|
|
467
500
|
}, [isLoading, isRowsEmpty]);
|
|
468
501
|
|
|
469
502
|
// Single tab-like discriminator drives the body - loader, empty, or content.
|
|
470
|
-
// While loading OR while bridging the loading
|
|
503
|
+
// While loading OR while bridging the loading-to-empty grace window, the
|
|
471
504
|
// loader stays visible to avoid the brief "no results" flash.
|
|
472
505
|
const view: "loader" | "empty" | "content" =
|
|
473
506
|
isLoading || (isRowsEmpty && !showEmptyState)
|
|
@@ -476,12 +509,40 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
476
509
|
? "empty"
|
|
477
510
|
: "content";
|
|
478
511
|
|
|
512
|
+
useIsomorphicLayoutEffect(() => {
|
|
513
|
+
if (view !== "loader") return;
|
|
514
|
+
const row = skeletonRowRef.current;
|
|
515
|
+
if (!row) return;
|
|
516
|
+
|
|
517
|
+
const measureRow = () => {
|
|
518
|
+
commitSkeletonRowHeight(Math.ceil(row.getBoundingClientRect().height));
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
measureRow();
|
|
522
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
523
|
+
|
|
524
|
+
const observer = new ResizeObserver(measureRow);
|
|
525
|
+
observer.observe(row);
|
|
526
|
+
|
|
527
|
+
return () => {
|
|
528
|
+
observer.disconnect();
|
|
529
|
+
};
|
|
530
|
+
}, [commitSkeletonRowHeight, view]);
|
|
531
|
+
|
|
479
532
|
const selectedCount = rowsSelectable
|
|
480
533
|
? table.getSelectedRowModel().flatRows.length
|
|
481
534
|
: 0;
|
|
482
535
|
const headerGroups = table.getHeaderGroups();
|
|
483
536
|
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
484
537
|
|
|
538
|
+
const skeletonRowCount =
|
|
539
|
+
bodyViewportHeight > 0 && skeletonRowHeight > 0
|
|
540
|
+
? Math.max(
|
|
541
|
+
MIN_SKELETON_ROWS,
|
|
542
|
+
Math.floor(bodyViewportHeight / skeletonRowHeight),
|
|
543
|
+
)
|
|
544
|
+
: MIN_SKELETON_ROWS;
|
|
545
|
+
|
|
485
546
|
const columnExtraWidths = useMemo<Record<string, number>>(() => {
|
|
486
547
|
const extraSpace = Math.max(0, bodyViewportWidth - totalWidth);
|
|
487
548
|
if (extraSpace <= 0) return EMPTY_COLUMN_EXTRA_WIDTHS;
|
|
@@ -511,14 +572,24 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
511
572
|
return next;
|
|
512
573
|
}, [bodyViewportWidth, totalWidth, visibleLeafColumns]);
|
|
513
574
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
575
|
+
const tableWidth = Math.max(totalWidth, bodyViewportWidth);
|
|
576
|
+
|
|
577
|
+
// Match the table element width to the measured column widths. This keeps
|
|
578
|
+
// header, body, and skeleton rows on the same sizing contract during resize.
|
|
517
579
|
const tableSizingStyle: CSSProperties = useMemo(
|
|
518
|
-
() => ({ width:
|
|
519
|
-
[totalWidth],
|
|
580
|
+
() => ({ width: `${tableWidth}px`, minWidth: `${totalWidth}px` }),
|
|
581
|
+
[tableWidth, totalWidth],
|
|
520
582
|
);
|
|
521
583
|
|
|
584
|
+
const bodySkeletonStyle: CSSProperties = {
|
|
585
|
+
height: "var(--spacing-9)",
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const selectSkeletonStyle: CSSProperties = {
|
|
589
|
+
width: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
590
|
+
height: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
591
|
+
};
|
|
592
|
+
|
|
522
593
|
const tableClass = clsx(
|
|
523
594
|
s.table,
|
|
524
595
|
showVerticalBorders && s.table_vertical_borders,
|
|
@@ -588,38 +659,50 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
588
659
|
{view === "loader" && (
|
|
589
660
|
<table className={tableClass} style={tableSizingStyle}>
|
|
590
661
|
<tbody>
|
|
591
|
-
{Array.from({ length:
|
|
592
|
-
(_,
|
|
593
|
-
<tr
|
|
594
|
-
{
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
);
|
|
662
|
+
{Array.from({ length: skeletonRowCount }).map(
|
|
663
|
+
(_, rowIndex) => (
|
|
664
|
+
<tr
|
|
665
|
+
key={`skeleton-${rowIndex}`}
|
|
666
|
+
ref={rowIndex === 0 ? skeletonRowRef : undefined}
|
|
667
|
+
>
|
|
668
|
+
{visibleLeafColumns.map((column) => {
|
|
669
|
+
const isPinned = column.getIsPinned();
|
|
670
|
+
const isLastPinnedCol =
|
|
671
|
+
isPinned && isLastPinned(column);
|
|
672
|
+
const isSelectColumn = column.id === SELECT_COLUMN_ID;
|
|
603
673
|
|
|
604
674
|
return (
|
|
605
675
|
<td
|
|
606
|
-
key={
|
|
676
|
+
key={column.id}
|
|
607
677
|
className={clsx(
|
|
608
678
|
isSelectColumn && s.col_select,
|
|
609
679
|
isPinned === "left" && s.col_pinned_left,
|
|
610
680
|
isPinned === "right" && s.col_pinned_right,
|
|
611
681
|
isLastPinnedCol && s.col_pinned_last,
|
|
612
682
|
)}
|
|
613
|
-
style={
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
683
|
+
style={getColumnStyle(
|
|
684
|
+
column,
|
|
685
|
+
column.getSize(),
|
|
686
|
+
columnExtraWidths[column.id] ?? 0,
|
|
687
|
+
)}
|
|
688
|
+
>
|
|
689
|
+
<div className={s.td_inner}>
|
|
690
|
+
<Loader
|
|
691
|
+
variant="skeleton"
|
|
692
|
+
className={
|
|
693
|
+
isSelectColumn
|
|
694
|
+
? s.loading_select_skeleton
|
|
695
|
+
: undefined
|
|
696
|
+
}
|
|
697
|
+
style={
|
|
698
|
+
isSelectColumn
|
|
699
|
+
? selectSkeletonStyle
|
|
700
|
+
: bodySkeletonStyle
|
|
701
|
+
}
|
|
702
|
+
/>
|
|
703
|
+
</div>
|
|
704
|
+
</td>
|
|
705
|
+
);
|
|
623
706
|
})}
|
|
624
707
|
</tr>
|
|
625
708
|
),
|
|
@@ -633,7 +716,11 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
633
716
|
</div>
|
|
634
717
|
)}
|
|
635
718
|
{view === "content" && (
|
|
636
|
-
<table
|
|
719
|
+
<table
|
|
720
|
+
className={tableClass}
|
|
721
|
+
style={tableSizingStyle}
|
|
722
|
+
ref={bodyTableRef}
|
|
723
|
+
>
|
|
637
724
|
<tbody>
|
|
638
725
|
{rows.map((row) => (
|
|
639
726
|
<BodyRow<TData>
|
|
@@ -666,6 +753,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
666
753
|
onPaginationChange={onPaginationChange}
|
|
667
754
|
pageSizeOptions={pageSizeOptions}
|
|
668
755
|
showPageSizeSelect={showPageSizeSelect}
|
|
756
|
+
isLoading={view === "loader"}
|
|
669
757
|
totalCount={totalCount}
|
|
670
758
|
rowLabel={rowLabel}
|
|
671
759
|
rowsSelectable={rowsSelectable}
|