@crispy-seed/data-table 0.11.2 → 0.12.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/dist/index.cjs +300 -57
- package/dist/index.d.ts +14 -0
- package/dist/index.js +300 -57
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1179,6 +1179,51 @@ function isStringArray(value) {
|
|
|
1179
1179
|
/** Removes the view with this `id`. */ function removeDataTableView(views, id) {
|
|
1180
1180
|
return views.filter((view)=>view.id !== id);
|
|
1181
1181
|
}
|
|
1182
|
+
// -- row virtualization ---------------------------------------------------------------
|
|
1183
|
+
// `virtualized` renders only the rows inside (and a little around) the scroll
|
|
1184
|
+
// container's viewport, with two spacer rows standing in for the rest so the scroll
|
|
1185
|
+
// height -- and therefore the scrollbar -- still matches the full data set. Rows are a
|
|
1186
|
+
// fixed height, so which rows are visible is plain arithmetic on `scrollTop`; no
|
|
1187
|
+
// per-row measurement, no ResizeObserver per row.
|
|
1188
|
+
/** The recipe's own row height (`--seed-dimension-x9` = 36px, `row.height`). */ const DEFAULT_VIRTUAL_ROW_HEIGHT = 36;
|
|
1189
|
+
/** Rows rendered beyond each edge of the viewport, so a fast scroll never outruns the
|
|
1190
|
+
* render and flashes an empty gap. */ const DEFAULT_VIRTUAL_OVERSCAN = 8;
|
|
1191
|
+
/** Distance from the bottom (px) at which `onLoadMore` fires in virtualized mode --
|
|
1192
|
+
* the same 160px look-ahead the IntersectionObserver's rootMargin gives a non-
|
|
1193
|
+
* virtualized table. */ const LOAD_MORE_THRESHOLD = 160;
|
|
1194
|
+
/**
|
|
1195
|
+
* Which slice `[start, end)` of `count` uniform-height rows to render for a viewport,
|
|
1196
|
+
* plus the heights of the two spacers that keep the total scroll height at
|
|
1197
|
+
* `count * rowHeight` (`topSpacer + rendered rows + bottomSpacer`). Pure -- the hook
|
|
1198
|
+
* measures the DOM, this decides.
|
|
1199
|
+
*/ function computeVirtualWindow(input) {
|
|
1200
|
+
const { count, rowHeight, viewportHeight, overscan } = input;
|
|
1201
|
+
if (count <= 0 || rowHeight <= 0) return {
|
|
1202
|
+
start: 0,
|
|
1203
|
+
end: 0,
|
|
1204
|
+
topSpacer: 0,
|
|
1205
|
+
bottomSpacer: 0
|
|
1206
|
+
};
|
|
1207
|
+
const scrollTop = Math.max(0, input.scrollTop);
|
|
1208
|
+
const firstVisible = Math.floor(scrollTop / rowHeight);
|
|
1209
|
+
const lastVisibleExclusive = Math.ceil((scrollTop + Math.max(0, viewportHeight)) / rowHeight);
|
|
1210
|
+
const start = Math.min(count, Math.max(0, firstVisible - overscan));
|
|
1211
|
+
const end = Math.min(count, Math.max(start, lastVisibleExclusive + overscan));
|
|
1212
|
+
return {
|
|
1213
|
+
start,
|
|
1214
|
+
end,
|
|
1215
|
+
topSpacer: start * rowHeight,
|
|
1216
|
+
bottomSpacer: (count - end) * rowHeight
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Scroll-position replacement for the sentinel row's IntersectionObserver: a
|
|
1221
|
+
* virtualized table can't rely on a sentinel (it would be windowed out along with the
|
|
1222
|
+
* rows), so "close enough to the bottom" is read straight off the scroll container.
|
|
1223
|
+
* The 1px slop matches `useDataTableScroll`'s own sub-pixel tolerance.
|
|
1224
|
+
*/ function isNearBottom(scrollTop, clientHeight, scrollHeight, threshold = LOAD_MORE_THRESHOLD) {
|
|
1225
|
+
return scrollTop + clientHeight >= scrollHeight - threshold - 1;
|
|
1226
|
+
}
|
|
1182
1227
|
const EMPTY_STRINGS = [];
|
|
1183
1228
|
const EMPTY_RECORD = {};
|
|
1184
1229
|
/**
|
|
@@ -1295,13 +1340,15 @@ const EMPTY_RECORD = {};
|
|
|
1295
1340
|
})), [
|
|
1296
1341
|
setColumnWidths
|
|
1297
1342
|
]);
|
|
1298
|
-
const moveColumn = React.useCallback((key, direction)=>setRawOrder((current)=>moveColumnKey(current ?? columnKeys, key, direction)), [
|
|
1343
|
+
const moveColumn = React.useCallback((key, direction)=>setRawOrder((current)=>moveColumnKey(reconcileColumnOrder(current ?? columnKeys, columnKeys, actionsColumnKey), key, direction)), [
|
|
1299
1344
|
setRawOrder,
|
|
1300
|
-
columnKeys
|
|
1345
|
+
columnKeys,
|
|
1346
|
+
actionsColumnKey
|
|
1301
1347
|
]);
|
|
1302
|
-
const reorderColumn = React.useCallback((dragKey, dropKey)=>setRawOrder((current)=>reorderColumnKey(current ?? columnKeys, dragKey, dropKey)), [
|
|
1348
|
+
const reorderColumn = React.useCallback((dragKey, dropKey)=>setRawOrder((current)=>reorderColumnKey(reconcileColumnOrder(current ?? columnKeys, columnKeys, actionsColumnKey), dragKey, dropKey)), [
|
|
1303
1349
|
setRawOrder,
|
|
1304
|
-
columnKeys
|
|
1350
|
+
columnKeys,
|
|
1351
|
+
actionsColumnKey
|
|
1305
1352
|
]);
|
|
1306
1353
|
// -- drag-to-reorder ---------------------------------------------------------------
|
|
1307
1354
|
// Live state only (which column is currently being dragged) -- the styled layer's
|
|
@@ -1469,11 +1516,11 @@ function sameCell(a, id, key) {
|
|
|
1469
1516
|
key
|
|
1470
1517
|
});
|
|
1471
1518
|
result.then(()=>{
|
|
1472
|
-
setPendingCell(null);
|
|
1473
|
-
setEditingCell(null);
|
|
1519
|
+
setPendingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1520
|
+
setEditingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1474
1521
|
}, (err)=>{
|
|
1475
|
-
setPendingCell(null);
|
|
1476
|
-
setEditingCell(null);
|
|
1522
|
+
setPendingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1523
|
+
setEditingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1477
1524
|
setEditError({
|
|
1478
1525
|
id,
|
|
1479
1526
|
key,
|
|
@@ -1510,7 +1557,7 @@ const ROOT_MARGIN = "160px";
|
|
|
1510
1557
|
* wires this to useDataTableScroll's own scroll container) so a BOUNDED table
|
|
1511
1558
|
* triggers off its own internal scroll, not the browsing viewport.
|
|
1512
1559
|
*/ function useDataTableInfiniteScroll(props) {
|
|
1513
|
-
const { onLoadMore, hasMore = true, loadingMore: loadingMoreProp, scrollRootRef } = props;
|
|
1560
|
+
const { onLoadMore, hasMore = true, loadingMore: loadingMoreProp, scrollRootRef, virtualized = false } = props;
|
|
1514
1561
|
const [internalLoadingMore, setInternalLoadingMore] = React.useState(false);
|
|
1515
1562
|
const loadingMore = loadingMoreProp ?? internalLoadingMore;
|
|
1516
1563
|
// Mirrors the latest props into a ref the observer callback reads -- the callback
|
|
@@ -1550,6 +1597,7 @@ const ROOT_MARGIN = "160px";
|
|
|
1550
1597
|
const [sentinelNode, setSentinelNode] = React.useState(null);
|
|
1551
1598
|
const sentinelRef = React.useCallback((node)=>setSentinelNode(node), []);
|
|
1552
1599
|
React.useEffect(()=>{
|
|
1600
|
+
if (virtualized) return;
|
|
1553
1601
|
if (!sentinelNode || !onLoadMore || typeof IntersectionObserver === "undefined") return;
|
|
1554
1602
|
// Effects run after every ref callback of the SAME commit (including
|
|
1555
1603
|
// `scrollRootRef`'s own, set by useDataTableScroll's `scrollAreaRef`) regardless
|
|
@@ -1570,6 +1618,23 @@ const ROOT_MARGIN = "160px";
|
|
|
1570
1618
|
sentinelNode,
|
|
1571
1619
|
onLoadMore,
|
|
1572
1620
|
scrollRootRef,
|
|
1621
|
+
triggerLoadMore,
|
|
1622
|
+
virtualized
|
|
1623
|
+
]);
|
|
1624
|
+
// Virtualized mode's trigger: near the bottom of the scroll container (same gate --
|
|
1625
|
+
// `hasMore`, not already loading -- via `triggerLoadMore`). Called on every scroll
|
|
1626
|
+
// and again after the row count / loading state changes (a page that lands while
|
|
1627
|
+
// the user is still at the bottom, or too short to overflow, must chain the next
|
|
1628
|
+
// load rather than wait for a scroll that can never come).
|
|
1629
|
+
const checkScrollPosition = React.useCallback(()=>{
|
|
1630
|
+
const element = scrollRootRef?.current;
|
|
1631
|
+
if (!virtualized || !element) return;
|
|
1632
|
+
if (isNearBottom(element.scrollTop, element.clientHeight, element.scrollHeight)) {
|
|
1633
|
+
triggerLoadMore();
|
|
1634
|
+
}
|
|
1635
|
+
}, [
|
|
1636
|
+
virtualized,
|
|
1637
|
+
scrollRootRef,
|
|
1573
1638
|
triggerLoadMore
|
|
1574
1639
|
]);
|
|
1575
1640
|
return {
|
|
@@ -1583,7 +1648,9 @@ const ROOT_MARGIN = "160px";
|
|
|
1583
1648
|
/** Fires `onLoadMore` directly, subject to the same `hasMore`/`loadingMore` gate
|
|
1584
1649
|
* the observer itself uses -- exposed for a consumer that wants a manual retry
|
|
1585
1650
|
* affordance (e.g. after a failed load) rather than only the observer's own
|
|
1586
|
-
* scroll trigger. */ triggerLoadMore
|
|
1651
|
+
* scroll trigger. */ triggerLoadMore,
|
|
1652
|
+
/** Virtualized mode's scroll-position check (no-op otherwise); `useDataTable`
|
|
1653
|
+
* calls it from the scroll container's `onScroll`. */ checkScrollPosition
|
|
1587
1654
|
};
|
|
1588
1655
|
}
|
|
1589
1656
|
|
|
@@ -1595,19 +1662,53 @@ const ROOT_MARGIN = "160px";
|
|
|
1595
1662
|
* real ref to a real scrollable element, re-measured on every scroll (and whenever the
|
|
1596
1663
|
* styled layer calls `remeasure`, e.g. after a column resize/reorder changes the
|
|
1597
1664
|
* scrollable width without the element itself ever firing a `scroll` event).
|
|
1598
|
-
*/ function useDataTableScroll() {
|
|
1665
|
+
*/ function useDataTableScroll(props = {}) {
|
|
1666
|
+
const { virtualized = false, rowHeight = DEFAULT_VIRTUAL_ROW_HEIGHT } = props;
|
|
1667
|
+
const overscan = props.overscan ?? DEFAULT_VIRTUAL_OVERSCAN;
|
|
1599
1668
|
const elementRef = React.useRef(null);
|
|
1600
1669
|
const [scrolledStart, setScrolledStart] = React.useState(false);
|
|
1601
1670
|
const [scrolledEnd, setScrolledEnd] = React.useState(false);
|
|
1671
|
+
const [viewport, setViewport] = React.useState({
|
|
1672
|
+
scrollTop: 0,
|
|
1673
|
+
viewportHeight: 0
|
|
1674
|
+
});
|
|
1602
1675
|
const remeasure = React.useCallback(()=>{
|
|
1603
1676
|
const element = elementRef.current;
|
|
1604
1677
|
if (!element) return;
|
|
1678
|
+
if (virtualized) {
|
|
1679
|
+
// Row 0 starts below the sticky header, so measure from there. Snapping to whole
|
|
1680
|
+
// rows keeps re-renders to one per row scrolled (not one per scroll event); the
|
|
1681
|
+
// extra `rowHeight` of viewport makes up for the rows the snap rounded away.
|
|
1682
|
+
const headerHeight = element.querySelector("thead")?.offsetHeight ?? 0;
|
|
1683
|
+
const scrollTop = Math.max(0, element.scrollTop - headerHeight);
|
|
1684
|
+
const snapped = Math.floor(scrollTop / rowHeight) * rowHeight;
|
|
1685
|
+
const viewportHeight = element.clientHeight + rowHeight;
|
|
1686
|
+
setViewport((prev)=>prev.scrollTop === snapped && prev.viewportHeight === viewportHeight ? prev : {
|
|
1687
|
+
scrollTop: snapped,
|
|
1688
|
+
viewportHeight
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1605
1691
|
setScrolledStart(element.scrollLeft > 0);
|
|
1606
1692
|
// A 1px slop: sub-pixel layout can leave `scrollLeft + clientWidth` a fraction
|
|
1607
1693
|
// short of `scrollWidth` even when fully scrolled, which would otherwise leave
|
|
1608
1694
|
// the trailing shadow stuck on forever.
|
|
1609
1695
|
setScrolledEnd(element.scrollLeft + element.clientWidth < element.scrollWidth - 1);
|
|
1610
|
-
}, [
|
|
1696
|
+
}, [
|
|
1697
|
+
virtualized,
|
|
1698
|
+
rowHeight
|
|
1699
|
+
]);
|
|
1700
|
+
// The container can resize without scrolling (window resize, a bounded parent
|
|
1701
|
+
// changing height) -- the viewport height feeds the window, so re-measure then too.
|
|
1702
|
+
React.useEffect(()=>{
|
|
1703
|
+
const element = elementRef.current;
|
|
1704
|
+
if (!virtualized || !element || typeof ResizeObserver === "undefined") return;
|
|
1705
|
+
const observer = new ResizeObserver(()=>remeasure());
|
|
1706
|
+
observer.observe(element);
|
|
1707
|
+
return ()=>observer.disconnect();
|
|
1708
|
+
}, [
|
|
1709
|
+
virtualized,
|
|
1710
|
+
remeasure
|
|
1711
|
+
]);
|
|
1611
1712
|
const scrollAreaRef = React.useCallback((node)=>{
|
|
1612
1713
|
elementRef.current = node;
|
|
1613
1714
|
if (node) remeasure();
|
|
@@ -1623,6 +1724,12 @@ const ROOT_MARGIN = "160px";
|
|
|
1623
1724
|
scrolledEnd,
|
|
1624
1725
|
onScroll,
|
|
1625
1726
|
remeasure,
|
|
1727
|
+
/** Row-virtualization state; `enabled: false` (the default) means render every row. */ virtual: {
|
|
1728
|
+
enabled: virtualized,
|
|
1729
|
+
rowHeight,
|
|
1730
|
+
overscan,
|
|
1731
|
+
...viewport
|
|
1732
|
+
},
|
|
1626
1733
|
// The raw ref object itself (not just the callback ref above) -- useDataTable.ts
|
|
1627
1734
|
// hands this to useDataTableInfiniteScroll as the IntersectionObserver `root` for
|
|
1628
1735
|
// its sentinel row, so a scrolled/bounded table (DATATABLE-ATTIO-SPEC.md's
|
|
@@ -2021,7 +2128,7 @@ function useDataTable(props) {
|
|
|
2021
2128
|
const toolbarFilter = useDataTableToolbarFilter(props);
|
|
2022
2129
|
const columns = useDataTableColumns(props);
|
|
2023
2130
|
const columnCalc = useDataTableColumnCalc(props);
|
|
2024
|
-
const scroll = useDataTableScroll();
|
|
2131
|
+
const scroll = useDataTableScroll(props);
|
|
2025
2132
|
const edit = useDataTableEdit(props);
|
|
2026
2133
|
// Feature C (#12) -- which single cell (if any) is showing its read-only "show
|
|
2027
2134
|
// full content" overlay. Independent of `edit` above -- see
|
|
@@ -2035,6 +2142,28 @@ function useDataTable(props) {
|
|
|
2035
2142
|
...props,
|
|
2036
2143
|
scrollRootRef: scroll.elementRef
|
|
2037
2144
|
});
|
|
2145
|
+
// Virtualized mode swaps the sentinel's IntersectionObserver for a scroll-position
|
|
2146
|
+
// check: on every scroll (below), and again whenever the row count or loading state
|
|
2147
|
+
// changes -- new rows can leave the user still at the bottom.
|
|
2148
|
+
const { checkScrollPosition } = infiniteScroll;
|
|
2149
|
+
const rowCount = props.rowIds?.length;
|
|
2150
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: rowCount/loadingMore/hasMore are the triggers, not values read in the effect
|
|
2151
|
+
React.useEffect(()=>{
|
|
2152
|
+
checkScrollPosition();
|
|
2153
|
+
}, [
|
|
2154
|
+
checkScrollPosition,
|
|
2155
|
+
rowCount,
|
|
2156
|
+
infiniteScroll.loadingMore,
|
|
2157
|
+
infiniteScroll.hasMore
|
|
2158
|
+
]);
|
|
2159
|
+
const { onScroll: measureScroll } = scroll;
|
|
2160
|
+
const onScroll = React.useCallback(()=>{
|
|
2161
|
+
measureScroll();
|
|
2162
|
+
checkScrollPosition();
|
|
2163
|
+
}, [
|
|
2164
|
+
measureScroll,
|
|
2165
|
+
checkScrollPosition
|
|
2166
|
+
]);
|
|
2038
2167
|
// Unifies `sort` (the header's own single click-to-sort column, above) with
|
|
2039
2168
|
// `toolbarSort.toolbarSorts` (the toolbar Sort popover's ordered list, above) into
|
|
2040
2169
|
// ONE state instead of two -- before this, a header click only ever moved `sort`,
|
|
@@ -2146,6 +2275,7 @@ function useDataTable(props) {
|
|
|
2146
2275
|
...edit,
|
|
2147
2276
|
...peek,
|
|
2148
2277
|
...infiniteScroll,
|
|
2278
|
+
onScroll,
|
|
2149
2279
|
rootProps: domUtils.elementProps({
|
|
2150
2280
|
role: "table"
|
|
2151
2281
|
})
|
|
@@ -2188,7 +2318,7 @@ function useDataTableRowContext({ strict = true } = {}) {
|
|
|
2188
2318
|
return context;
|
|
2189
2319
|
}
|
|
2190
2320
|
const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
2191
|
-
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
2321
|
+
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, virtualized, rowHeight, overscan, ...otherProps } = props;
|
|
2192
2322
|
const api = useDataTable({
|
|
2193
2323
|
sort,
|
|
2194
2324
|
defaultSort,
|
|
@@ -2230,7 +2360,10 @@ const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
|
2230
2360
|
onEdit,
|
|
2231
2361
|
onLoadMore,
|
|
2232
2362
|
hasMore,
|
|
2233
|
-
loadingMore
|
|
2363
|
+
loadingMore,
|
|
2364
|
+
virtualized,
|
|
2365
|
+
rowHeight,
|
|
2366
|
+
overscan
|
|
2234
2367
|
});
|
|
2235
2368
|
return /*#__PURE__*/ jsxRuntime.jsx(DataTableProvider, {
|
|
2236
2369
|
value: api,
|
|
@@ -2313,9 +2446,20 @@ DataTableCell$1.displayName = "DataTableCell";
|
|
|
2313
2446
|
|
|
2314
2447
|
const EMPTY_VIEWS = [];
|
|
2315
2448
|
function readStoredViews(storageKey) {
|
|
2316
|
-
if (!storageKey || typeof window === "undefined") return
|
|
2449
|
+
if (!storageKey || typeof window === "undefined") return null;
|
|
2317
2450
|
try {
|
|
2318
|
-
|
|
2451
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
2452
|
+
if (raw === null) return null;
|
|
2453
|
+
// Only a well-formed array counts as "stored" -- `[]` is a real answer (the user
|
|
2454
|
+
// deleted every view), whereas garbage/non-array JSON is treated as absent so the
|
|
2455
|
+
// defaults still seed.
|
|
2456
|
+
let parsed;
|
|
2457
|
+
try {
|
|
2458
|
+
parsed = JSON.parse(raw);
|
|
2459
|
+
} catch {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
return Array.isArray(parsed) ? deserializeDataTableViews(raw) : null;
|
|
2319
2463
|
} catch {
|
|
2320
2464
|
// A private-browsing tab / blocked storage throws on access, not just on quota --
|
|
2321
2465
|
// this hook degrades to in-memory-only rather than crash the table over it.
|
|
@@ -2353,7 +2497,7 @@ function readStoredViews(storageKey) {
|
|
|
2353
2497
|
// Nothing in storage yet (first run, or no `storageKey` at all) -- seed from
|
|
2354
2498
|
// `defaultViews` instead of an empty list, matching every other hook's own
|
|
2355
2499
|
// `defaultProp` semantics.
|
|
2356
|
-
initialViewsRef.current = stored
|
|
2500
|
+
initialViewsRef.current = stored !== null ? stored : defaultViews;
|
|
2357
2501
|
}
|
|
2358
2502
|
}
|
|
2359
2503
|
const [views = EMPTY_VIEWS, setViews] = useControllableState$1({
|
|
@@ -4349,6 +4493,10 @@ const { ClassNamesProvider, withContext: withContext$1, useClassNames } = create
|
|
|
4349
4493
|
});
|
|
4350
4494
|
}
|
|
4351
4495
|
|
|
4496
|
+
/** Attribute on the floating-ui-portaled option list of the `select`/`reference`/`actor`
|
|
4497
|
+
* editors. The portal lands in `document.body`, outside the cell and its expand overlay,
|
|
4498
|
+
* so `body-cells.tsx`'s outside-click detector checks for this marker to avoid reading an
|
|
4499
|
+
* option click as "outside" and cancelling the edit before it commits. */ const EDITOR_PORTAL_ATTR = "data-datatable-editor-portal";
|
|
4352
4500
|
/** The raw value a live editor reads, distinct from `DataTableColumn.value` (the
|
|
4353
4501
|
* accessor sort/search/filter/export share): `editValue` returns whatever TYPE the
|
|
4354
4502
|
* editor itself expects -- a real `boolean` for `editor: "switch"`/`type:
|
|
@@ -4497,6 +4645,7 @@ function SelectEditor({ column, value, pending, onCommit, onCancel }) {
|
|
|
4497
4645
|
]
|
|
4498
4646
|
}),
|
|
4499
4647
|
/*#__PURE__*/ jsxRuntime.jsx(SelectPositioner, {
|
|
4648
|
+
[EDITOR_PORTAL_ATTR]: "",
|
|
4500
4649
|
children: /*#__PURE__*/ jsxRuntime.jsxs(SelectContent, {
|
|
4501
4650
|
children: [
|
|
4502
4651
|
searchable && /*#__PURE__*/ jsxRuntime.jsx(SelectSearchField, {
|
|
@@ -4572,6 +4721,7 @@ function ReferenceEditor({ column, value, pending, onCommit, onCancel }) {
|
|
|
4572
4721
|
]
|
|
4573
4722
|
}),
|
|
4574
4723
|
/*#__PURE__*/ jsxRuntime.jsx(SelectPositioner, {
|
|
4724
|
+
[EDITOR_PORTAL_ATTR]: "",
|
|
4575
4725
|
children: /*#__PURE__*/ jsxRuntime.jsxs(SelectContent, {
|
|
4576
4726
|
children: [
|
|
4577
4727
|
/*#__PURE__*/ jsxRuntime.jsx(SelectSearchField, {
|
|
@@ -5187,8 +5337,14 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5187
5337
|
function isInside(target) {
|
|
5188
5338
|
return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false) || (expandOverlayRef.current?.contains(target) ?? false);
|
|
5189
5339
|
}
|
|
5340
|
+
// The select/reference/actor editor's option list is portaled to `document.body` by
|
|
5341
|
+
// floating-ui, outside both `cellRef` and `expandOverlayRef`; `editors.tsx` marks its
|
|
5342
|
+
// positioner with `EDITOR_PORTAL_ATTR`, so a click on an option is not an outside click.
|
|
5343
|
+
function isInsideEditorPortal(target) {
|
|
5344
|
+
return target instanceof Element && target.closest(`[${EDITOR_PORTAL_ATTR}]`) != null;
|
|
5345
|
+
}
|
|
5190
5346
|
function handlePointerDown(event) {
|
|
5191
|
-
if (!isInside(event.target)) {
|
|
5347
|
+
if (!isInside(event.target) && !isInsideEditorPortal(event.target)) {
|
|
5192
5348
|
setOverlayOpen(false);
|
|
5193
5349
|
setPopoverStyle(null);
|
|
5194
5350
|
closePeek();
|
|
@@ -11062,7 +11218,13 @@ const toDateOrNull = (v)=>{
|
|
|
11062
11218
|
return f.op === "before" ? d < cd : d > cd;
|
|
11063
11219
|
}
|
|
11064
11220
|
const t = asText(raw).toLowerCase();
|
|
11065
|
-
if (f.op === "isanyof")
|
|
11221
|
+
if (f.op === "isanyof") {
|
|
11222
|
+
const wanted = (f.values ?? []).map((v)=>v.toLowerCase());
|
|
11223
|
+
const cells = Array.isArray(raw) ? raw.map((x)=>asText(x).toLowerCase()) : [
|
|
11224
|
+
t
|
|
11225
|
+
];
|
|
11226
|
+
return cells.some((c)=>wanted.includes(c));
|
|
11227
|
+
}
|
|
11066
11228
|
const v = f.value.trim().toLowerCase();
|
|
11067
11229
|
if (f.op === "contains") return t.includes(v);
|
|
11068
11230
|
if (f.op === "doesnotcontain") return !t.includes(v);
|
|
@@ -11710,7 +11872,7 @@ function RowCount({ shown, total, countLabel }) {
|
|
|
11710
11872
|
}
|
|
11711
11873
|
|
|
11712
11874
|
function DataTableInner(props, ref) {
|
|
11713
|
-
const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, bulkActions, clearSelectionLabel, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, cellContextMenu = true, onViewEditHistory, cellContextMenuItems, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, ...restProps } = props;
|
|
11875
|
+
const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, bulkActions, clearSelectionLabel, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, cellContextMenu = true, onViewEditHistory, cellContextMenuItems, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, virtualized = false, rowHeight, overscan, ...restProps } = props;
|
|
11714
11876
|
const classNames = dataTable({
|
|
11715
11877
|
surface,
|
|
11716
11878
|
interactive
|
|
@@ -11883,6 +12045,9 @@ function DataTableInner(props, ref) {
|
|
|
11883
12045
|
onLoadMore: onLoadMore,
|
|
11884
12046
|
hasMore: hasMore,
|
|
11885
12047
|
loadingMore: loadingMore,
|
|
12048
|
+
virtualized: virtualized,
|
|
12049
|
+
rowHeight: rowHeight,
|
|
12050
|
+
overscan: overscan,
|
|
11886
12051
|
...restProps,
|
|
11887
12052
|
children: [
|
|
11888
12053
|
onColumnsChange && /*#__PURE__*/ jsxRuntime.jsx(ColumnsChangeEffect, {
|
|
@@ -11915,7 +12080,9 @@ function DataTableInner(props, ref) {
|
|
|
11915
12080
|
}),
|
|
11916
12081
|
/*#__PURE__*/ jsxRuntime.jsx(DataTableScrollBody, {
|
|
11917
12082
|
scrollProps: scrollProps,
|
|
12083
|
+
virtualized: virtualized,
|
|
11918
12084
|
children: /*#__PURE__*/ jsxRuntime.jsxs(DataTableTable, {
|
|
12085
|
+
"aria-rowcount": virtualized ? sortedData.length + 1 : undefined,
|
|
11919
12086
|
children: [
|
|
11920
12087
|
/*#__PURE__*/ jsxRuntime.jsx(DataTableColGroup, {
|
|
11921
12088
|
columns: columns,
|
|
@@ -11971,39 +12138,47 @@ function DataTableInner(props, ref) {
|
|
|
11971
12138
|
})
|
|
11972
12139
|
}) : /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
11973
12140
|
children: [
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12141
|
+
/*#__PURE__*/ jsxRuntime.jsx(WindowedRows, {
|
|
12142
|
+
rows: sortedData,
|
|
12143
|
+
virtualized: virtualized,
|
|
12144
|
+
colSpan: totalCols,
|
|
12145
|
+
children: (windowRows, firstIndex)=>/*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
12146
|
+
children: windowRows.map((row, offset)=>/*#__PURE__*/ jsxRuntime.jsxs(DataTableRow, {
|
|
12147
|
+
rowId: row.id,
|
|
12148
|
+
"aria-rowindex": virtualized ? firstIndex + offset + 2 : undefined,
|
|
12149
|
+
"data-clickable": onRowClick ? "" : undefined,
|
|
12150
|
+
onClick: onRowClick ? ()=>onRowClick(row) : undefined,
|
|
12151
|
+
children: [
|
|
12152
|
+
selectable && /*#__PURE__*/ jsxRuntime.jsx(SelectionCell, {
|
|
12153
|
+
// The row itself carries `onClick={onRowClick}` above, and a
|
|
12154
|
+
// native click on this cell's checkbox bubbles up through it
|
|
12155
|
+
// (React attaches DataTableRow's handler on the <tr>, which
|
|
12156
|
+
// sits between the checkbox and anything a consumer could stop
|
|
12157
|
+
// it from at -- so this has to live here, not in the consumer).
|
|
12158
|
+
// Without this, checking a row's box also fires `onRowClick`.
|
|
12159
|
+
onClick: (event)=>event.stopPropagation(),
|
|
12160
|
+
stickyFirst: stickyFirst,
|
|
12161
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(RowCheckbox, {
|
|
12162
|
+
label: getRowSelectionLabel(row),
|
|
12163
|
+
checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON
|
|
12164
|
+
})
|
|
12165
|
+
}),
|
|
12166
|
+
/*#__PURE__*/ jsxRuntime.jsx(ColumnBodyCells, {
|
|
12167
|
+
columns: columns,
|
|
12168
|
+
row: row,
|
|
12169
|
+
classNames: classNames,
|
|
12170
|
+
locale: locale,
|
|
12171
|
+
selectable: !!selectable,
|
|
12172
|
+
stickyFirst: stickyFirst,
|
|
12173
|
+
stickyLast: resolvedStickyLast,
|
|
12174
|
+
cellContextMenu: cellContextMenu,
|
|
12175
|
+
onViewEditHistory: onViewEditHistory,
|
|
12176
|
+
cellContextMenuItems: cellContextMenuItems
|
|
12177
|
+
})
|
|
12178
|
+
]
|
|
12179
|
+
}, row.id))
|
|
12180
|
+
})
|
|
12181
|
+
}),
|
|
12007
12182
|
onLoadMore && /*#__PURE__*/ jsxRuntime.jsx(InfiniteScrollRow, {
|
|
12008
12183
|
colSpan: totalCols,
|
|
12009
12184
|
classNames: classNames,
|
|
@@ -12104,7 +12279,69 @@ const DataTableTable = withContext$1(DataTableTable$1, "table");
|
|
|
12104
12279
|
* out of sync between the three. A column with no explicit width gets a bare `<col>`
|
|
12105
12280
|
* (no `style`): `table-layout: fixed`'s own algorithm gives every such column an equal
|
|
12106
12281
|
* share of whatever space is left, matching the old flex grid's `flex: 1 1 0%`
|
|
12107
|
-
* default with no extra code needed here. */
|
|
12282
|
+
* default with no extra code needed here. */ /**
|
|
12283
|
+
* Row virtualization's render slice. Lives inside `DataTableRoot` (it reads the
|
|
12284
|
+
* windowing state off context, which `DataTableInner` above can't) and hands its
|
|
12285
|
+
* children the visible `[start, end)` slice of `rows` plus that slice's first index,
|
|
12286
|
+
* flanked by a spacer row each side that stands in for everything cut off -- so the
|
|
12287
|
+
* scroll height stays `rows.length * rowHeight` and the scrollbar matches the full
|
|
12288
|
+
* data set. `virtualized` off: every row, no spacers, the pre-0.12 output exactly.
|
|
12289
|
+
*/ function WindowedRows({ rows, virtualized, colSpan, children }) {
|
|
12290
|
+
const { virtual } = useDataTableContext();
|
|
12291
|
+
const windowed = React__namespace.useMemo(()=>virtualized ? computeVirtualWindow({
|
|
12292
|
+
count: rows.length,
|
|
12293
|
+
rowHeight: virtual.rowHeight,
|
|
12294
|
+
scrollTop: virtual.scrollTop,
|
|
12295
|
+
viewportHeight: virtual.viewportHeight,
|
|
12296
|
+
overscan: virtual.overscan
|
|
12297
|
+
}) : null, [
|
|
12298
|
+
virtualized,
|
|
12299
|
+
rows.length,
|
|
12300
|
+
virtual.rowHeight,
|
|
12301
|
+
virtual.scrollTop,
|
|
12302
|
+
virtual.viewportHeight,
|
|
12303
|
+
virtual.overscan
|
|
12304
|
+
]);
|
|
12305
|
+
if (!windowed) return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
12306
|
+
children: children(rows, 0)
|
|
12307
|
+
});
|
|
12308
|
+
return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
12309
|
+
children: [
|
|
12310
|
+
windowed.topSpacer > 0 && /*#__PURE__*/ jsxRuntime.jsx(VirtualSpacerRow, {
|
|
12311
|
+
height: windowed.topSpacer,
|
|
12312
|
+
colSpan: colSpan
|
|
12313
|
+
}),
|
|
12314
|
+
children(rows.slice(windowed.start, windowed.end), windowed.start),
|
|
12315
|
+
windowed.bottomSpacer > 0 && /*#__PURE__*/ jsxRuntime.jsx(VirtualSpacerRow, {
|
|
12316
|
+
height: windowed.bottomSpacer,
|
|
12317
|
+
colSpan: colSpan
|
|
12318
|
+
})
|
|
12319
|
+
]
|
|
12320
|
+
});
|
|
12321
|
+
}
|
|
12322
|
+
/** A plain `<tr><td colSpan>` of a given height -- deliberately NOT a `DataTableRow`/
|
|
12323
|
+
* `DataTableCell` (no hover tint, no cell chrome, no border, no 36px min height).
|
|
12324
|
+
* `colSpan` covers every column, so the `<colgroup>` still drives widths and sticky
|
|
12325
|
+
* columns are unaffected. */ function VirtualSpacerRow({ height, colSpan }) {
|
|
12326
|
+
return /*#__PURE__*/ jsxRuntime.jsx("tr", {
|
|
12327
|
+
"data-virtual-spacer": "",
|
|
12328
|
+
style: {
|
|
12329
|
+
height
|
|
12330
|
+
},
|
|
12331
|
+
children: /*#__PURE__*/ jsxRuntime.jsx("td", {
|
|
12332
|
+
"aria-hidden": "true",
|
|
12333
|
+
colSpan: colSpan,
|
|
12334
|
+
style: {
|
|
12335
|
+
height,
|
|
12336
|
+
padding: 0,
|
|
12337
|
+
border: 0,
|
|
12338
|
+
display: "table-cell",
|
|
12339
|
+
lineHeight: 0
|
|
12340
|
+
}
|
|
12341
|
+
})
|
|
12342
|
+
});
|
|
12343
|
+
}
|
|
12344
|
+
function DataTableColGroup({ columns, selectable, stickyFirst }) {
|
|
12108
12345
|
// MUST use the same pinned-first ordering every OTHER row (header/body/footer)
|
|
12109
12346
|
// renders in (`useEffectiveColumns`'s own doc comment) -- a `<colgroup>`'s `<col>`s
|
|
12110
12347
|
// are matched positionally to each row's cells, so if a pinned column visually moves
|
|
@@ -12179,7 +12416,7 @@ const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
|
|
|
12179
12416
|
// to clip), so it never introduces a second vertical scrollbar next to whatever
|
|
12180
12417
|
// ambient container the whole DataTable already sits in.
|
|
12181
12418
|
const DataTableScrollBodyBase = withContext$1(Primitive.div, "scrollArea");
|
|
12182
|
-
function DataTableScrollBody({ children, scrollProps }) {
|
|
12419
|
+
function DataTableScrollBody({ children, virtualized, scrollProps }) {
|
|
12183
12420
|
const { scrollAreaRef, scrolledStart, scrolledEnd, onScroll } = useDataTableContext();
|
|
12184
12421
|
const { className: scrollClassName, style: scrollStyle, ...otherScrollProps } = scrollProps ?? {};
|
|
12185
12422
|
return /*#__PURE__*/ jsxRuntime.jsx(DataTableScrollBodyBase, {
|
|
@@ -12187,7 +12424,13 @@ function DataTableScrollBody({ children, scrollProps }) {
|
|
|
12187
12424
|
ref: scrollAreaRef,
|
|
12188
12425
|
onScroll: onScroll,
|
|
12189
12426
|
className: scrollClassName,
|
|
12190
|
-
|
|
12427
|
+
// Virtualized: the browser's scroll anchoring would try to hold the visible rows
|
|
12428
|
+
// steady as the spacer heights change under it, fighting the scroll position
|
|
12429
|
+
// this windowing itself derives from -- turn it off.
|
|
12430
|
+
style: virtualized ? {
|
|
12431
|
+
...scrollStyle,
|
|
12432
|
+
overflowAnchor: "none"
|
|
12433
|
+
} : scrollStyle,
|
|
12191
12434
|
"data-scrolled-start": domUtils.dataAttr(scrolledStart),
|
|
12192
12435
|
"data-scrolled-end": domUtils.dataAttr(scrolledEnd),
|
|
12193
12436
|
children: children
|
package/dist/index.d.ts
CHANGED
|
@@ -534,6 +534,20 @@ interface DataTableProps<TRow extends {
|
|
|
534
534
|
* 됩니다 -- 새 prop 없이 기존 `style` 전달만으로 두 모드 다 지원됩니다.
|
|
535
535
|
*/
|
|
536
536
|
scrollProps?: Omit<react.ComponentPropsWithoutRef<"div">, "ref" | "onScroll">;
|
|
537
|
+
/**
|
|
538
|
+
* 행 가상화(windowing): 스크롤 컨테이너 뷰포트 근처의 행만 렌더링하고 나머지는 위/아래
|
|
539
|
+
* spacer 행으로 대체해 전체 스크롤 높이를 유지합니다. 수천 행 이상에서 사용하세요.
|
|
540
|
+
* 기본값(`false`)이면 지금까지처럼 모든 행을 렌더링합니다. 켜면 모든 행이 `rowHeight`로
|
|
541
|
+
* 동일한 높이여야 하고, 스크롤이 생기도록 높이 제한(`style={{ maxHeight }}` 등)이
|
|
542
|
+
* 필요합니다. 무한 스크롤(`onLoadMore`)은 sentinel 대신 스크롤 위치로 트리거됩니다.
|
|
543
|
+
* @default false
|
|
544
|
+
*/
|
|
545
|
+
virtualized?: boolean;
|
|
546
|
+
/** `virtualized`일 때 한 행의 높이(px)입니다. 레시피의 행 높이(36px)와 일치해야 합니다.
|
|
547
|
+
* @default 36 */
|
|
548
|
+
rowHeight?: number;
|
|
549
|
+
/** `virtualized`일 때 뷰포트 위/아래로 추가 렌더링할 행 수입니다. @default 8 */
|
|
550
|
+
overscan?: number;
|
|
537
551
|
}
|
|
538
552
|
/**
|
|
539
553
|
* @see https://seed-design.io/react/components/data-table
|
package/dist/index.js
CHANGED
|
@@ -1160,6 +1160,51 @@ function isStringArray(value) {
|
|
|
1160
1160
|
/** Removes the view with this `id`. */ function removeDataTableView(views, id) {
|
|
1161
1161
|
return views.filter((view)=>view.id !== id);
|
|
1162
1162
|
}
|
|
1163
|
+
// -- row virtualization ---------------------------------------------------------------
|
|
1164
|
+
// `virtualized` renders only the rows inside (and a little around) the scroll
|
|
1165
|
+
// container's viewport, with two spacer rows standing in for the rest so the scroll
|
|
1166
|
+
// height -- and therefore the scrollbar -- still matches the full data set. Rows are a
|
|
1167
|
+
// fixed height, so which rows are visible is plain arithmetic on `scrollTop`; no
|
|
1168
|
+
// per-row measurement, no ResizeObserver per row.
|
|
1169
|
+
/** The recipe's own row height (`--seed-dimension-x9` = 36px, `row.height`). */ const DEFAULT_VIRTUAL_ROW_HEIGHT = 36;
|
|
1170
|
+
/** Rows rendered beyond each edge of the viewport, so a fast scroll never outruns the
|
|
1171
|
+
* render and flashes an empty gap. */ const DEFAULT_VIRTUAL_OVERSCAN = 8;
|
|
1172
|
+
/** Distance from the bottom (px) at which `onLoadMore` fires in virtualized mode --
|
|
1173
|
+
* the same 160px look-ahead the IntersectionObserver's rootMargin gives a non-
|
|
1174
|
+
* virtualized table. */ const LOAD_MORE_THRESHOLD = 160;
|
|
1175
|
+
/**
|
|
1176
|
+
* Which slice `[start, end)` of `count` uniform-height rows to render for a viewport,
|
|
1177
|
+
* plus the heights of the two spacers that keep the total scroll height at
|
|
1178
|
+
* `count * rowHeight` (`topSpacer + rendered rows + bottomSpacer`). Pure -- the hook
|
|
1179
|
+
* measures the DOM, this decides.
|
|
1180
|
+
*/ function computeVirtualWindow(input) {
|
|
1181
|
+
const { count, rowHeight, viewportHeight, overscan } = input;
|
|
1182
|
+
if (count <= 0 || rowHeight <= 0) return {
|
|
1183
|
+
start: 0,
|
|
1184
|
+
end: 0,
|
|
1185
|
+
topSpacer: 0,
|
|
1186
|
+
bottomSpacer: 0
|
|
1187
|
+
};
|
|
1188
|
+
const scrollTop = Math.max(0, input.scrollTop);
|
|
1189
|
+
const firstVisible = Math.floor(scrollTop / rowHeight);
|
|
1190
|
+
const lastVisibleExclusive = Math.ceil((scrollTop + Math.max(0, viewportHeight)) / rowHeight);
|
|
1191
|
+
const start = Math.min(count, Math.max(0, firstVisible - overscan));
|
|
1192
|
+
const end = Math.min(count, Math.max(start, lastVisibleExclusive + overscan));
|
|
1193
|
+
return {
|
|
1194
|
+
start,
|
|
1195
|
+
end,
|
|
1196
|
+
topSpacer: start * rowHeight,
|
|
1197
|
+
bottomSpacer: (count - end) * rowHeight
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Scroll-position replacement for the sentinel row's IntersectionObserver: a
|
|
1202
|
+
* virtualized table can't rely on a sentinel (it would be windowed out along with the
|
|
1203
|
+
* rows), so "close enough to the bottom" is read straight off the scroll container.
|
|
1204
|
+
* The 1px slop matches `useDataTableScroll`'s own sub-pixel tolerance.
|
|
1205
|
+
*/ function isNearBottom(scrollTop, clientHeight, scrollHeight, threshold = LOAD_MORE_THRESHOLD) {
|
|
1206
|
+
return scrollTop + clientHeight >= scrollHeight - threshold - 1;
|
|
1207
|
+
}
|
|
1163
1208
|
const EMPTY_STRINGS = [];
|
|
1164
1209
|
const EMPTY_RECORD = {};
|
|
1165
1210
|
/**
|
|
@@ -1276,13 +1321,15 @@ const EMPTY_RECORD = {};
|
|
|
1276
1321
|
})), [
|
|
1277
1322
|
setColumnWidths
|
|
1278
1323
|
]);
|
|
1279
|
-
const moveColumn = useCallback((key, direction)=>setRawOrder((current)=>moveColumnKey(current ?? columnKeys, key, direction)), [
|
|
1324
|
+
const moveColumn = useCallback((key, direction)=>setRawOrder((current)=>moveColumnKey(reconcileColumnOrder(current ?? columnKeys, columnKeys, actionsColumnKey), key, direction)), [
|
|
1280
1325
|
setRawOrder,
|
|
1281
|
-
columnKeys
|
|
1326
|
+
columnKeys,
|
|
1327
|
+
actionsColumnKey
|
|
1282
1328
|
]);
|
|
1283
|
-
const reorderColumn = useCallback((dragKey, dropKey)=>setRawOrder((current)=>reorderColumnKey(current ?? columnKeys, dragKey, dropKey)), [
|
|
1329
|
+
const reorderColumn = useCallback((dragKey, dropKey)=>setRawOrder((current)=>reorderColumnKey(reconcileColumnOrder(current ?? columnKeys, columnKeys, actionsColumnKey), dragKey, dropKey)), [
|
|
1284
1330
|
setRawOrder,
|
|
1285
|
-
columnKeys
|
|
1331
|
+
columnKeys,
|
|
1332
|
+
actionsColumnKey
|
|
1286
1333
|
]);
|
|
1287
1334
|
// -- drag-to-reorder ---------------------------------------------------------------
|
|
1288
1335
|
// Live state only (which column is currently being dragged) -- the styled layer's
|
|
@@ -1450,11 +1497,11 @@ function sameCell(a, id, key) {
|
|
|
1450
1497
|
key
|
|
1451
1498
|
});
|
|
1452
1499
|
result.then(()=>{
|
|
1453
|
-
setPendingCell(null);
|
|
1454
|
-
setEditingCell(null);
|
|
1500
|
+
setPendingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1501
|
+
setEditingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1455
1502
|
}, (err)=>{
|
|
1456
|
-
setPendingCell(null);
|
|
1457
|
-
setEditingCell(null);
|
|
1503
|
+
setPendingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1504
|
+
setEditingCell((c)=>sameCell(c, id, key) ? null : c);
|
|
1458
1505
|
setEditError({
|
|
1459
1506
|
id,
|
|
1460
1507
|
key,
|
|
@@ -1491,7 +1538,7 @@ const ROOT_MARGIN = "160px";
|
|
|
1491
1538
|
* wires this to useDataTableScroll's own scroll container) so a BOUNDED table
|
|
1492
1539
|
* triggers off its own internal scroll, not the browsing viewport.
|
|
1493
1540
|
*/ function useDataTableInfiniteScroll(props) {
|
|
1494
|
-
const { onLoadMore, hasMore = true, loadingMore: loadingMoreProp, scrollRootRef } = props;
|
|
1541
|
+
const { onLoadMore, hasMore = true, loadingMore: loadingMoreProp, scrollRootRef, virtualized = false } = props;
|
|
1495
1542
|
const [internalLoadingMore, setInternalLoadingMore] = useState(false);
|
|
1496
1543
|
const loadingMore = loadingMoreProp ?? internalLoadingMore;
|
|
1497
1544
|
// Mirrors the latest props into a ref the observer callback reads -- the callback
|
|
@@ -1531,6 +1578,7 @@ const ROOT_MARGIN = "160px";
|
|
|
1531
1578
|
const [sentinelNode, setSentinelNode] = useState(null);
|
|
1532
1579
|
const sentinelRef = useCallback((node)=>setSentinelNode(node), []);
|
|
1533
1580
|
useEffect(()=>{
|
|
1581
|
+
if (virtualized) return;
|
|
1534
1582
|
if (!sentinelNode || !onLoadMore || typeof IntersectionObserver === "undefined") return;
|
|
1535
1583
|
// Effects run after every ref callback of the SAME commit (including
|
|
1536
1584
|
// `scrollRootRef`'s own, set by useDataTableScroll's `scrollAreaRef`) regardless
|
|
@@ -1551,6 +1599,23 @@ const ROOT_MARGIN = "160px";
|
|
|
1551
1599
|
sentinelNode,
|
|
1552
1600
|
onLoadMore,
|
|
1553
1601
|
scrollRootRef,
|
|
1602
|
+
triggerLoadMore,
|
|
1603
|
+
virtualized
|
|
1604
|
+
]);
|
|
1605
|
+
// Virtualized mode's trigger: near the bottom of the scroll container (same gate --
|
|
1606
|
+
// `hasMore`, not already loading -- via `triggerLoadMore`). Called on every scroll
|
|
1607
|
+
// and again after the row count / loading state changes (a page that lands while
|
|
1608
|
+
// the user is still at the bottom, or too short to overflow, must chain the next
|
|
1609
|
+
// load rather than wait for a scroll that can never come).
|
|
1610
|
+
const checkScrollPosition = useCallback(()=>{
|
|
1611
|
+
const element = scrollRootRef?.current;
|
|
1612
|
+
if (!virtualized || !element) return;
|
|
1613
|
+
if (isNearBottom(element.scrollTop, element.clientHeight, element.scrollHeight)) {
|
|
1614
|
+
triggerLoadMore();
|
|
1615
|
+
}
|
|
1616
|
+
}, [
|
|
1617
|
+
virtualized,
|
|
1618
|
+
scrollRootRef,
|
|
1554
1619
|
triggerLoadMore
|
|
1555
1620
|
]);
|
|
1556
1621
|
return {
|
|
@@ -1564,7 +1629,9 @@ const ROOT_MARGIN = "160px";
|
|
|
1564
1629
|
/** Fires `onLoadMore` directly, subject to the same `hasMore`/`loadingMore` gate
|
|
1565
1630
|
* the observer itself uses -- exposed for a consumer that wants a manual retry
|
|
1566
1631
|
* affordance (e.g. after a failed load) rather than only the observer's own
|
|
1567
|
-
* scroll trigger. */ triggerLoadMore
|
|
1632
|
+
* scroll trigger. */ triggerLoadMore,
|
|
1633
|
+
/** Virtualized mode's scroll-position check (no-op otherwise); `useDataTable`
|
|
1634
|
+
* calls it from the scroll container's `onScroll`. */ checkScrollPosition
|
|
1568
1635
|
};
|
|
1569
1636
|
}
|
|
1570
1637
|
|
|
@@ -1576,19 +1643,53 @@ const ROOT_MARGIN = "160px";
|
|
|
1576
1643
|
* real ref to a real scrollable element, re-measured on every scroll (and whenever the
|
|
1577
1644
|
* styled layer calls `remeasure`, e.g. after a column resize/reorder changes the
|
|
1578
1645
|
* scrollable width without the element itself ever firing a `scroll` event).
|
|
1579
|
-
*/ function useDataTableScroll() {
|
|
1646
|
+
*/ function useDataTableScroll(props = {}) {
|
|
1647
|
+
const { virtualized = false, rowHeight = DEFAULT_VIRTUAL_ROW_HEIGHT } = props;
|
|
1648
|
+
const overscan = props.overscan ?? DEFAULT_VIRTUAL_OVERSCAN;
|
|
1580
1649
|
const elementRef = useRef(null);
|
|
1581
1650
|
const [scrolledStart, setScrolledStart] = useState(false);
|
|
1582
1651
|
const [scrolledEnd, setScrolledEnd] = useState(false);
|
|
1652
|
+
const [viewport, setViewport] = useState({
|
|
1653
|
+
scrollTop: 0,
|
|
1654
|
+
viewportHeight: 0
|
|
1655
|
+
});
|
|
1583
1656
|
const remeasure = useCallback(()=>{
|
|
1584
1657
|
const element = elementRef.current;
|
|
1585
1658
|
if (!element) return;
|
|
1659
|
+
if (virtualized) {
|
|
1660
|
+
// Row 0 starts below the sticky header, so measure from there. Snapping to whole
|
|
1661
|
+
// rows keeps re-renders to one per row scrolled (not one per scroll event); the
|
|
1662
|
+
// extra `rowHeight` of viewport makes up for the rows the snap rounded away.
|
|
1663
|
+
const headerHeight = element.querySelector("thead")?.offsetHeight ?? 0;
|
|
1664
|
+
const scrollTop = Math.max(0, element.scrollTop - headerHeight);
|
|
1665
|
+
const snapped = Math.floor(scrollTop / rowHeight) * rowHeight;
|
|
1666
|
+
const viewportHeight = element.clientHeight + rowHeight;
|
|
1667
|
+
setViewport((prev)=>prev.scrollTop === snapped && prev.viewportHeight === viewportHeight ? prev : {
|
|
1668
|
+
scrollTop: snapped,
|
|
1669
|
+
viewportHeight
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1586
1672
|
setScrolledStart(element.scrollLeft > 0);
|
|
1587
1673
|
// A 1px slop: sub-pixel layout can leave `scrollLeft + clientWidth` a fraction
|
|
1588
1674
|
// short of `scrollWidth` even when fully scrolled, which would otherwise leave
|
|
1589
1675
|
// the trailing shadow stuck on forever.
|
|
1590
1676
|
setScrolledEnd(element.scrollLeft + element.clientWidth < element.scrollWidth - 1);
|
|
1591
|
-
}, [
|
|
1677
|
+
}, [
|
|
1678
|
+
virtualized,
|
|
1679
|
+
rowHeight
|
|
1680
|
+
]);
|
|
1681
|
+
// The container can resize without scrolling (window resize, a bounded parent
|
|
1682
|
+
// changing height) -- the viewport height feeds the window, so re-measure then too.
|
|
1683
|
+
useEffect(()=>{
|
|
1684
|
+
const element = elementRef.current;
|
|
1685
|
+
if (!virtualized || !element || typeof ResizeObserver === "undefined") return;
|
|
1686
|
+
const observer = new ResizeObserver(()=>remeasure());
|
|
1687
|
+
observer.observe(element);
|
|
1688
|
+
return ()=>observer.disconnect();
|
|
1689
|
+
}, [
|
|
1690
|
+
virtualized,
|
|
1691
|
+
remeasure
|
|
1692
|
+
]);
|
|
1592
1693
|
const scrollAreaRef = useCallback((node)=>{
|
|
1593
1694
|
elementRef.current = node;
|
|
1594
1695
|
if (node) remeasure();
|
|
@@ -1604,6 +1705,12 @@ const ROOT_MARGIN = "160px";
|
|
|
1604
1705
|
scrolledEnd,
|
|
1605
1706
|
onScroll,
|
|
1606
1707
|
remeasure,
|
|
1708
|
+
/** Row-virtualization state; `enabled: false` (the default) means render every row. */ virtual: {
|
|
1709
|
+
enabled: virtualized,
|
|
1710
|
+
rowHeight,
|
|
1711
|
+
overscan,
|
|
1712
|
+
...viewport
|
|
1713
|
+
},
|
|
1607
1714
|
// The raw ref object itself (not just the callback ref above) -- useDataTable.ts
|
|
1608
1715
|
// hands this to useDataTableInfiniteScroll as the IntersectionObserver `root` for
|
|
1609
1716
|
// its sentinel row, so a scrolled/bounded table (DATATABLE-ATTIO-SPEC.md's
|
|
@@ -2002,7 +2109,7 @@ function useDataTable(props) {
|
|
|
2002
2109
|
const toolbarFilter = useDataTableToolbarFilter(props);
|
|
2003
2110
|
const columns = useDataTableColumns(props);
|
|
2004
2111
|
const columnCalc = useDataTableColumnCalc(props);
|
|
2005
|
-
const scroll = useDataTableScroll();
|
|
2112
|
+
const scroll = useDataTableScroll(props);
|
|
2006
2113
|
const edit = useDataTableEdit(props);
|
|
2007
2114
|
// Feature C (#12) -- which single cell (if any) is showing its read-only "show
|
|
2008
2115
|
// full content" overlay. Independent of `edit` above -- see
|
|
@@ -2016,6 +2123,28 @@ function useDataTable(props) {
|
|
|
2016
2123
|
...props,
|
|
2017
2124
|
scrollRootRef: scroll.elementRef
|
|
2018
2125
|
});
|
|
2126
|
+
// Virtualized mode swaps the sentinel's IntersectionObserver for a scroll-position
|
|
2127
|
+
// check: on every scroll (below), and again whenever the row count or loading state
|
|
2128
|
+
// changes -- new rows can leave the user still at the bottom.
|
|
2129
|
+
const { checkScrollPosition } = infiniteScroll;
|
|
2130
|
+
const rowCount = props.rowIds?.length;
|
|
2131
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: rowCount/loadingMore/hasMore are the triggers, not values read in the effect
|
|
2132
|
+
useEffect(()=>{
|
|
2133
|
+
checkScrollPosition();
|
|
2134
|
+
}, [
|
|
2135
|
+
checkScrollPosition,
|
|
2136
|
+
rowCount,
|
|
2137
|
+
infiniteScroll.loadingMore,
|
|
2138
|
+
infiniteScroll.hasMore
|
|
2139
|
+
]);
|
|
2140
|
+
const { onScroll: measureScroll } = scroll;
|
|
2141
|
+
const onScroll = useCallback(()=>{
|
|
2142
|
+
measureScroll();
|
|
2143
|
+
checkScrollPosition();
|
|
2144
|
+
}, [
|
|
2145
|
+
measureScroll,
|
|
2146
|
+
checkScrollPosition
|
|
2147
|
+
]);
|
|
2019
2148
|
// Unifies `sort` (the header's own single click-to-sort column, above) with
|
|
2020
2149
|
// `toolbarSort.toolbarSorts` (the toolbar Sort popover's ordered list, above) into
|
|
2021
2150
|
// ONE state instead of two -- before this, a header click only ever moved `sort`,
|
|
@@ -2127,6 +2256,7 @@ function useDataTable(props) {
|
|
|
2127
2256
|
...edit,
|
|
2128
2257
|
...peek,
|
|
2129
2258
|
...infiniteScroll,
|
|
2259
|
+
onScroll,
|
|
2130
2260
|
rootProps: elementProps({
|
|
2131
2261
|
role: "table"
|
|
2132
2262
|
})
|
|
@@ -2169,7 +2299,7 @@ function useDataTableRowContext({ strict = true } = {}) {
|
|
|
2169
2299
|
return context;
|
|
2170
2300
|
}
|
|
2171
2301
|
const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
2172
|
-
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
2302
|
+
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, virtualized, rowHeight, overscan, ...otherProps } = props;
|
|
2173
2303
|
const api = useDataTable({
|
|
2174
2304
|
sort,
|
|
2175
2305
|
defaultSort,
|
|
@@ -2211,7 +2341,10 @@ const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
|
2211
2341
|
onEdit,
|
|
2212
2342
|
onLoadMore,
|
|
2213
2343
|
hasMore,
|
|
2214
|
-
loadingMore
|
|
2344
|
+
loadingMore,
|
|
2345
|
+
virtualized,
|
|
2346
|
+
rowHeight,
|
|
2347
|
+
overscan
|
|
2215
2348
|
});
|
|
2216
2349
|
return /*#__PURE__*/ jsx(DataTableProvider, {
|
|
2217
2350
|
value: api,
|
|
@@ -2294,9 +2427,20 @@ DataTableCell$1.displayName = "DataTableCell";
|
|
|
2294
2427
|
|
|
2295
2428
|
const EMPTY_VIEWS = [];
|
|
2296
2429
|
function readStoredViews(storageKey) {
|
|
2297
|
-
if (!storageKey || typeof window === "undefined") return
|
|
2430
|
+
if (!storageKey || typeof window === "undefined") return null;
|
|
2298
2431
|
try {
|
|
2299
|
-
|
|
2432
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
2433
|
+
if (raw === null) return null;
|
|
2434
|
+
// Only a well-formed array counts as "stored" -- `[]` is a real answer (the user
|
|
2435
|
+
// deleted every view), whereas garbage/non-array JSON is treated as absent so the
|
|
2436
|
+
// defaults still seed.
|
|
2437
|
+
let parsed;
|
|
2438
|
+
try {
|
|
2439
|
+
parsed = JSON.parse(raw);
|
|
2440
|
+
} catch {
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
return Array.isArray(parsed) ? deserializeDataTableViews(raw) : null;
|
|
2300
2444
|
} catch {
|
|
2301
2445
|
// A private-browsing tab / blocked storage throws on access, not just on quota --
|
|
2302
2446
|
// this hook degrades to in-memory-only rather than crash the table over it.
|
|
@@ -2334,7 +2478,7 @@ function readStoredViews(storageKey) {
|
|
|
2334
2478
|
// Nothing in storage yet (first run, or no `storageKey` at all) -- seed from
|
|
2335
2479
|
// `defaultViews` instead of an empty list, matching every other hook's own
|
|
2336
2480
|
// `defaultProp` semantics.
|
|
2337
|
-
initialViewsRef.current = stored
|
|
2481
|
+
initialViewsRef.current = stored !== null ? stored : defaultViews;
|
|
2338
2482
|
}
|
|
2339
2483
|
}
|
|
2340
2484
|
const [views = EMPTY_VIEWS, setViews] = useControllableState$1({
|
|
@@ -4330,6 +4474,10 @@ const { ClassNamesProvider, withContext: withContext$1, useClassNames } = create
|
|
|
4330
4474
|
});
|
|
4331
4475
|
}
|
|
4332
4476
|
|
|
4477
|
+
/** Attribute on the floating-ui-portaled option list of the `select`/`reference`/`actor`
|
|
4478
|
+
* editors. The portal lands in `document.body`, outside the cell and its expand overlay,
|
|
4479
|
+
* so `body-cells.tsx`'s outside-click detector checks for this marker to avoid reading an
|
|
4480
|
+
* option click as "outside" and cancelling the edit before it commits. */ const EDITOR_PORTAL_ATTR = "data-datatable-editor-portal";
|
|
4333
4481
|
/** The raw value a live editor reads, distinct from `DataTableColumn.value` (the
|
|
4334
4482
|
* accessor sort/search/filter/export share): `editValue` returns whatever TYPE the
|
|
4335
4483
|
* editor itself expects -- a real `boolean` for `editor: "switch"`/`type:
|
|
@@ -4478,6 +4626,7 @@ function SelectEditor({ column, value, pending, onCommit, onCancel }) {
|
|
|
4478
4626
|
]
|
|
4479
4627
|
}),
|
|
4480
4628
|
/*#__PURE__*/ jsx(SelectPositioner, {
|
|
4629
|
+
[EDITOR_PORTAL_ATTR]: "",
|
|
4481
4630
|
children: /*#__PURE__*/ jsxs(SelectContent, {
|
|
4482
4631
|
children: [
|
|
4483
4632
|
searchable && /*#__PURE__*/ jsx(SelectSearchField, {
|
|
@@ -4553,6 +4702,7 @@ function ReferenceEditor({ column, value, pending, onCommit, onCancel }) {
|
|
|
4553
4702
|
]
|
|
4554
4703
|
}),
|
|
4555
4704
|
/*#__PURE__*/ jsx(SelectPositioner, {
|
|
4705
|
+
[EDITOR_PORTAL_ATTR]: "",
|
|
4556
4706
|
children: /*#__PURE__*/ jsxs(SelectContent, {
|
|
4557
4707
|
children: [
|
|
4558
4708
|
/*#__PURE__*/ jsx(SelectSearchField, {
|
|
@@ -5168,8 +5318,14 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
|
|
|
5168
5318
|
function isInside(target) {
|
|
5169
5319
|
return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false) || (expandOverlayRef.current?.contains(target) ?? false);
|
|
5170
5320
|
}
|
|
5321
|
+
// The select/reference/actor editor's option list is portaled to `document.body` by
|
|
5322
|
+
// floating-ui, outside both `cellRef` and `expandOverlayRef`; `editors.tsx` marks its
|
|
5323
|
+
// positioner with `EDITOR_PORTAL_ATTR`, so a click on an option is not an outside click.
|
|
5324
|
+
function isInsideEditorPortal(target) {
|
|
5325
|
+
return target instanceof Element && target.closest(`[${EDITOR_PORTAL_ATTR}]`) != null;
|
|
5326
|
+
}
|
|
5171
5327
|
function handlePointerDown(event) {
|
|
5172
|
-
if (!isInside(event.target)) {
|
|
5328
|
+
if (!isInside(event.target) && !isInsideEditorPortal(event.target)) {
|
|
5173
5329
|
setOverlayOpen(false);
|
|
5174
5330
|
setPopoverStyle(null);
|
|
5175
5331
|
closePeek();
|
|
@@ -11043,7 +11199,13 @@ const toDateOrNull = (v)=>{
|
|
|
11043
11199
|
return f.op === "before" ? d < cd : d > cd;
|
|
11044
11200
|
}
|
|
11045
11201
|
const t = asText(raw).toLowerCase();
|
|
11046
|
-
if (f.op === "isanyof")
|
|
11202
|
+
if (f.op === "isanyof") {
|
|
11203
|
+
const wanted = (f.values ?? []).map((v)=>v.toLowerCase());
|
|
11204
|
+
const cells = Array.isArray(raw) ? raw.map((x)=>asText(x).toLowerCase()) : [
|
|
11205
|
+
t
|
|
11206
|
+
];
|
|
11207
|
+
return cells.some((c)=>wanted.includes(c));
|
|
11208
|
+
}
|
|
11047
11209
|
const v = f.value.trim().toLowerCase();
|
|
11048
11210
|
if (f.op === "contains") return t.includes(v);
|
|
11049
11211
|
if (f.op === "doesnotcontain") return !t.includes(v);
|
|
@@ -11691,7 +11853,7 @@ function RowCount({ shown, total, countLabel }) {
|
|
|
11691
11853
|
}
|
|
11692
11854
|
|
|
11693
11855
|
function DataTableInner(props, ref) {
|
|
11694
|
-
const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, bulkActions, clearSelectionLabel, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, cellContextMenu = true, onViewEditHistory, cellContextMenuItems, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, ...restProps } = props;
|
|
11856
|
+
const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, bulkActions, clearSelectionLabel, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, cellContextMenu = true, onViewEditHistory, cellContextMenuItems, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, virtualized = false, rowHeight, overscan, ...restProps } = props;
|
|
11695
11857
|
const classNames = dataTable({
|
|
11696
11858
|
surface,
|
|
11697
11859
|
interactive
|
|
@@ -11864,6 +12026,9 @@ function DataTableInner(props, ref) {
|
|
|
11864
12026
|
onLoadMore: onLoadMore,
|
|
11865
12027
|
hasMore: hasMore,
|
|
11866
12028
|
loadingMore: loadingMore,
|
|
12029
|
+
virtualized: virtualized,
|
|
12030
|
+
rowHeight: rowHeight,
|
|
12031
|
+
overscan: overscan,
|
|
11867
12032
|
...restProps,
|
|
11868
12033
|
children: [
|
|
11869
12034
|
onColumnsChange && /*#__PURE__*/ jsx(ColumnsChangeEffect, {
|
|
@@ -11896,7 +12061,9 @@ function DataTableInner(props, ref) {
|
|
|
11896
12061
|
}),
|
|
11897
12062
|
/*#__PURE__*/ jsx(DataTableScrollBody, {
|
|
11898
12063
|
scrollProps: scrollProps,
|
|
12064
|
+
virtualized: virtualized,
|
|
11899
12065
|
children: /*#__PURE__*/ jsxs(DataTableTable, {
|
|
12066
|
+
"aria-rowcount": virtualized ? sortedData.length + 1 : undefined,
|
|
11900
12067
|
children: [
|
|
11901
12068
|
/*#__PURE__*/ jsx(DataTableColGroup, {
|
|
11902
12069
|
columns: columns,
|
|
@@ -11952,39 +12119,47 @@ function DataTableInner(props, ref) {
|
|
|
11952
12119
|
})
|
|
11953
12120
|
}) : /*#__PURE__*/ jsxs(Fragment, {
|
|
11954
12121
|
children: [
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
12122
|
+
/*#__PURE__*/ jsx(WindowedRows, {
|
|
12123
|
+
rows: sortedData,
|
|
12124
|
+
virtualized: virtualized,
|
|
12125
|
+
colSpan: totalCols,
|
|
12126
|
+
children: (windowRows, firstIndex)=>/*#__PURE__*/ jsx(Fragment, {
|
|
12127
|
+
children: windowRows.map((row, offset)=>/*#__PURE__*/ jsxs(DataTableRow, {
|
|
12128
|
+
rowId: row.id,
|
|
12129
|
+
"aria-rowindex": virtualized ? firstIndex + offset + 2 : undefined,
|
|
12130
|
+
"data-clickable": onRowClick ? "" : undefined,
|
|
12131
|
+
onClick: onRowClick ? ()=>onRowClick(row) : undefined,
|
|
12132
|
+
children: [
|
|
12133
|
+
selectable && /*#__PURE__*/ jsx(SelectionCell, {
|
|
12134
|
+
// The row itself carries `onClick={onRowClick}` above, and a
|
|
12135
|
+
// native click on this cell's checkbox bubbles up through it
|
|
12136
|
+
// (React attaches DataTableRow's handler on the <tr>, which
|
|
12137
|
+
// sits between the checkbox and anything a consumer could stop
|
|
12138
|
+
// it from at -- so this has to live here, not in the consumer).
|
|
12139
|
+
// Without this, checking a row's box also fires `onRowClick`.
|
|
12140
|
+
onClick: (event)=>event.stopPropagation(),
|
|
12141
|
+
stickyFirst: stickyFirst,
|
|
12142
|
+
children: /*#__PURE__*/ jsx(RowCheckbox, {
|
|
12143
|
+
label: getRowSelectionLabel(row),
|
|
12144
|
+
checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON
|
|
12145
|
+
})
|
|
12146
|
+
}),
|
|
12147
|
+
/*#__PURE__*/ jsx(ColumnBodyCells, {
|
|
12148
|
+
columns: columns,
|
|
12149
|
+
row: row,
|
|
12150
|
+
classNames: classNames,
|
|
12151
|
+
locale: locale,
|
|
12152
|
+
selectable: !!selectable,
|
|
12153
|
+
stickyFirst: stickyFirst,
|
|
12154
|
+
stickyLast: resolvedStickyLast,
|
|
12155
|
+
cellContextMenu: cellContextMenu,
|
|
12156
|
+
onViewEditHistory: onViewEditHistory,
|
|
12157
|
+
cellContextMenuItems: cellContextMenuItems
|
|
12158
|
+
})
|
|
12159
|
+
]
|
|
12160
|
+
}, row.id))
|
|
12161
|
+
})
|
|
12162
|
+
}),
|
|
11988
12163
|
onLoadMore && /*#__PURE__*/ jsx(InfiniteScrollRow, {
|
|
11989
12164
|
colSpan: totalCols,
|
|
11990
12165
|
classNames: classNames,
|
|
@@ -12085,7 +12260,69 @@ const DataTableTable = withContext$1(DataTableTable$1, "table");
|
|
|
12085
12260
|
* out of sync between the three. A column with no explicit width gets a bare `<col>`
|
|
12086
12261
|
* (no `style`): `table-layout: fixed`'s own algorithm gives every such column an equal
|
|
12087
12262
|
* share of whatever space is left, matching the old flex grid's `flex: 1 1 0%`
|
|
12088
|
-
* default with no extra code needed here. */
|
|
12263
|
+
* default with no extra code needed here. */ /**
|
|
12264
|
+
* Row virtualization's render slice. Lives inside `DataTableRoot` (it reads the
|
|
12265
|
+
* windowing state off context, which `DataTableInner` above can't) and hands its
|
|
12266
|
+
* children the visible `[start, end)` slice of `rows` plus that slice's first index,
|
|
12267
|
+
* flanked by a spacer row each side that stands in for everything cut off -- so the
|
|
12268
|
+
* scroll height stays `rows.length * rowHeight` and the scrollbar matches the full
|
|
12269
|
+
* data set. `virtualized` off: every row, no spacers, the pre-0.12 output exactly.
|
|
12270
|
+
*/ function WindowedRows({ rows, virtualized, colSpan, children }) {
|
|
12271
|
+
const { virtual } = useDataTableContext();
|
|
12272
|
+
const windowed = React.useMemo(()=>virtualized ? computeVirtualWindow({
|
|
12273
|
+
count: rows.length,
|
|
12274
|
+
rowHeight: virtual.rowHeight,
|
|
12275
|
+
scrollTop: virtual.scrollTop,
|
|
12276
|
+
viewportHeight: virtual.viewportHeight,
|
|
12277
|
+
overscan: virtual.overscan
|
|
12278
|
+
}) : null, [
|
|
12279
|
+
virtualized,
|
|
12280
|
+
rows.length,
|
|
12281
|
+
virtual.rowHeight,
|
|
12282
|
+
virtual.scrollTop,
|
|
12283
|
+
virtual.viewportHeight,
|
|
12284
|
+
virtual.overscan
|
|
12285
|
+
]);
|
|
12286
|
+
if (!windowed) return /*#__PURE__*/ jsx(Fragment, {
|
|
12287
|
+
children: children(rows, 0)
|
|
12288
|
+
});
|
|
12289
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
12290
|
+
children: [
|
|
12291
|
+
windowed.topSpacer > 0 && /*#__PURE__*/ jsx(VirtualSpacerRow, {
|
|
12292
|
+
height: windowed.topSpacer,
|
|
12293
|
+
colSpan: colSpan
|
|
12294
|
+
}),
|
|
12295
|
+
children(rows.slice(windowed.start, windowed.end), windowed.start),
|
|
12296
|
+
windowed.bottomSpacer > 0 && /*#__PURE__*/ jsx(VirtualSpacerRow, {
|
|
12297
|
+
height: windowed.bottomSpacer,
|
|
12298
|
+
colSpan: colSpan
|
|
12299
|
+
})
|
|
12300
|
+
]
|
|
12301
|
+
});
|
|
12302
|
+
}
|
|
12303
|
+
/** A plain `<tr><td colSpan>` of a given height -- deliberately NOT a `DataTableRow`/
|
|
12304
|
+
* `DataTableCell` (no hover tint, no cell chrome, no border, no 36px min height).
|
|
12305
|
+
* `colSpan` covers every column, so the `<colgroup>` still drives widths and sticky
|
|
12306
|
+
* columns are unaffected. */ function VirtualSpacerRow({ height, colSpan }) {
|
|
12307
|
+
return /*#__PURE__*/ jsx("tr", {
|
|
12308
|
+
"data-virtual-spacer": "",
|
|
12309
|
+
style: {
|
|
12310
|
+
height
|
|
12311
|
+
},
|
|
12312
|
+
children: /*#__PURE__*/ jsx("td", {
|
|
12313
|
+
"aria-hidden": "true",
|
|
12314
|
+
colSpan: colSpan,
|
|
12315
|
+
style: {
|
|
12316
|
+
height,
|
|
12317
|
+
padding: 0,
|
|
12318
|
+
border: 0,
|
|
12319
|
+
display: "table-cell",
|
|
12320
|
+
lineHeight: 0
|
|
12321
|
+
}
|
|
12322
|
+
})
|
|
12323
|
+
});
|
|
12324
|
+
}
|
|
12325
|
+
function DataTableColGroup({ columns, selectable, stickyFirst }) {
|
|
12089
12326
|
// MUST use the same pinned-first ordering every OTHER row (header/body/footer)
|
|
12090
12327
|
// renders in (`useEffectiveColumns`'s own doc comment) -- a `<colgroup>`'s `<col>`s
|
|
12091
12328
|
// are matched positionally to each row's cells, so if a pinned column visually moves
|
|
@@ -12160,7 +12397,7 @@ const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
|
|
|
12160
12397
|
// to clip), so it never introduces a second vertical scrollbar next to whatever
|
|
12161
12398
|
// ambient container the whole DataTable already sits in.
|
|
12162
12399
|
const DataTableScrollBodyBase = withContext$1(Primitive.div, "scrollArea");
|
|
12163
|
-
function DataTableScrollBody({ children, scrollProps }) {
|
|
12400
|
+
function DataTableScrollBody({ children, virtualized, scrollProps }) {
|
|
12164
12401
|
const { scrollAreaRef, scrolledStart, scrolledEnd, onScroll } = useDataTableContext();
|
|
12165
12402
|
const { className: scrollClassName, style: scrollStyle, ...otherScrollProps } = scrollProps ?? {};
|
|
12166
12403
|
return /*#__PURE__*/ jsx(DataTableScrollBodyBase, {
|
|
@@ -12168,7 +12405,13 @@ function DataTableScrollBody({ children, scrollProps }) {
|
|
|
12168
12405
|
ref: scrollAreaRef,
|
|
12169
12406
|
onScroll: onScroll,
|
|
12170
12407
|
className: scrollClassName,
|
|
12171
|
-
|
|
12408
|
+
// Virtualized: the browser's scroll anchoring would try to hold the visible rows
|
|
12409
|
+
// steady as the spacer heights change under it, fighting the scroll position
|
|
12410
|
+
// this windowing itself derives from -- turn it off.
|
|
12411
|
+
style: virtualized ? {
|
|
12412
|
+
...scrollStyle,
|
|
12413
|
+
overflowAnchor: "none"
|
|
12414
|
+
} : scrollStyle,
|
|
12172
12415
|
"data-scrolled-start": dataAttr(scrolledStart),
|
|
12173
12416
|
"data-scrolled-end": dataAttr(scrolledEnd),
|
|
12174
12417
|
children: children
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crispy-seed/data-table",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Self-contained, publishable build of seed-design's Attio-measured DataTable, ResizablePanel and RecordPreview — one install, one CSS import.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|