@vesture/react 0.2.7 → 0.2.9
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.css +71 -5
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +283 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -278,6 +278,7 @@ interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
|
278
278
|
}
|
|
279
279
|
declare function Alert({ variant, title: titleText, onDismiss, children, className, ...rest }: AlertProps): ReactElement;
|
|
280
280
|
|
|
281
|
+
type AggregateType = "sum" | "avg" | "count" | "min" | "max";
|
|
281
282
|
interface DataGridColumn<T> {
|
|
282
283
|
key: string;
|
|
283
284
|
header: ReactNode;
|
|
@@ -297,6 +298,8 @@ interface DataGridColumn<T> {
|
|
|
297
298
|
render?: (row: T) => ReactNode;
|
|
298
299
|
/** Value used when exporting this column; falls back to accessor, then the raw row value. Not derived from render, since render returns a ReactNode. */
|
|
299
300
|
exportValue?: (row: T) => string | number;
|
|
301
|
+
/** Computed per-group when the grid is grouped, and shown in this column's slot within the group header row. */
|
|
302
|
+
aggregate?: AggregateType | ((rows: T[]) => ReactNode);
|
|
300
303
|
}
|
|
301
304
|
type SortDirection = "asc" | "desc" | null;
|
|
302
305
|
interface SortState {
|
|
@@ -336,6 +339,16 @@ interface DataGridProps<T> {
|
|
|
336
339
|
onPageChange?: (page: number) => void;
|
|
337
340
|
/** Renders an "Export" button in the grid's toolbar that exports the current view to Excel. */
|
|
338
341
|
enableExport?: boolean;
|
|
342
|
+
/** Groups rows by this column key (single-level). undefined = no grouping. */
|
|
343
|
+
groupBy?: string;
|
|
344
|
+
defaultGroupBy?: string;
|
|
345
|
+
onGroupByChange?: (key: string | undefined) => void;
|
|
346
|
+
/** Which group keys are expanded. Default: all expanded. */
|
|
347
|
+
expandedGroups?: Set<string>;
|
|
348
|
+
defaultExpandedGroups?: Set<string>;
|
|
349
|
+
onExpandedGroupsChange?: (expanded: Set<string>) => void;
|
|
350
|
+
/** Height of a group header row; defaults to rowHeight. */
|
|
351
|
+
groupRowHeight?: number;
|
|
339
352
|
onRowClick?: (row: T, event: MouseEvent<HTMLDivElement>) => void;
|
|
340
353
|
onRowDoubleClick?: (row: T, event: MouseEvent<HTMLDivElement>) => void;
|
|
341
354
|
onCellClick?: (row: T, column: DataGridColumn<T>, event: MouseEvent<HTMLDivElement>) => void;
|
|
@@ -488,13 +501,17 @@ interface ComboboxProps {
|
|
|
488
501
|
disabled?: boolean;
|
|
489
502
|
invalid?: boolean;
|
|
490
503
|
loading?: boolean;
|
|
504
|
+
/** Number of filtered options at/above which the listbox switches to virtualized (windowed) rendering. */
|
|
505
|
+
virtualizationThreshold?: number;
|
|
506
|
+
/** Row height (px) used for virtualized listbox rows; defaults to the `.option` class's rendered height. */
|
|
507
|
+
optionHeight?: number;
|
|
491
508
|
id?: string;
|
|
492
509
|
className?: string;
|
|
493
510
|
"aria-label"?: string;
|
|
494
511
|
"aria-labelledby"?: string;
|
|
495
512
|
}
|
|
496
513
|
|
|
497
|
-
declare function Combobox({ options, multiple, value: controlledValue, defaultValue, onChange, onInputChange, filterOptions, placeholder, noOptionsMessage, disabled, invalid, loading, id, className, ...rest }: ComboboxProps): ReactElement;
|
|
514
|
+
declare function Combobox({ options, multiple, value: controlledValue, defaultValue, onChange, onInputChange, filterOptions, placeholder, noOptionsMessage, disabled, invalid, loading, virtualizationThreshold, optionHeight, id, className, ...rest }: ComboboxProps): ReactElement;
|
|
498
515
|
|
|
499
516
|
interface TreeNode<T = unknown> {
|
|
500
517
|
id: string;
|
package/dist/index.js
CHANGED
|
@@ -1116,12 +1116,18 @@ var body3 = "DataGrid_body__7xivx77";
|
|
|
1116
1116
|
var cell = "DataGrid_cell__7xivx79";
|
|
1117
1117
|
var checkboxCell = "DataGrid_checkboxCell__7xivx7a";
|
|
1118
1118
|
var container = "DataGrid_container__7xivx71";
|
|
1119
|
-
var editInput = "
|
|
1119
|
+
var editInput = "DataGrid_editInput__7xivx7v";
|
|
1120
1120
|
var emptyState = "DataGrid_emptyState__7xivx7b";
|
|
1121
1121
|
var filterCell = "DataGrid_filterCell__7xivx7j";
|
|
1122
1122
|
var filterInput = "DataGrid_filterInput__7xivx7k";
|
|
1123
1123
|
var filterRow = "DataGrid_filterRow__7xivx7i";
|
|
1124
1124
|
var gridWrapper = "DataGrid_gridWrapper__7xivx7m";
|
|
1125
|
+
var groupAggregateItem = "DataGrid_groupAggregateItem__7xivx7u";
|
|
1126
|
+
var groupAggregates = "DataGrid_groupAggregates__7xivx7t";
|
|
1127
|
+
var groupChevron = "DataGrid_groupChevron__7xivx7q";
|
|
1128
|
+
var groupCount = "DataGrid_groupCount__7xivx7s";
|
|
1129
|
+
var groupLabel = "DataGrid_groupLabel__7xivx7r";
|
|
1130
|
+
var groupRow = "DataGrid_groupRow__7xivx7p";
|
|
1125
1131
|
var headerButton = "DataGrid_headerButton__7xivx74";
|
|
1126
1132
|
var headerCell = "DataGrid_headerCell__7xivx73";
|
|
1127
1133
|
var headerRow = "DataGrid_headerRow__7xivx72";
|
|
@@ -1206,6 +1212,40 @@ function getCellValue(column, row3) {
|
|
|
1206
1212
|
}
|
|
1207
1213
|
return row3[column.key];
|
|
1208
1214
|
}
|
|
1215
|
+
function getGroupValue(row3, groupBy, column) {
|
|
1216
|
+
if (column) {
|
|
1217
|
+
return String(getCellValue(column, row3) ?? "");
|
|
1218
|
+
}
|
|
1219
|
+
return String(row3[groupBy] ?? "");
|
|
1220
|
+
}
|
|
1221
|
+
function formatAggregateNumber(value) {
|
|
1222
|
+
return Number.isInteger(value) ? value : value.toFixed(2);
|
|
1223
|
+
}
|
|
1224
|
+
function computeAggregate(column, rows) {
|
|
1225
|
+
const { aggregate } = column;
|
|
1226
|
+
if (!aggregate) return null;
|
|
1227
|
+
if (typeof aggregate === "function") {
|
|
1228
|
+
return aggregate(rows);
|
|
1229
|
+
}
|
|
1230
|
+
if (aggregate === "count") {
|
|
1231
|
+
return rows.length;
|
|
1232
|
+
}
|
|
1233
|
+
const values = rows.map((r) => Number(getCellValue(column, r)) || 0);
|
|
1234
|
+
switch (aggregate) {
|
|
1235
|
+
case "sum":
|
|
1236
|
+
return formatAggregateNumber(values.reduce((a, b) => a + b, 0));
|
|
1237
|
+
case "avg":
|
|
1238
|
+
return formatAggregateNumber(
|
|
1239
|
+
values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0
|
|
1240
|
+
);
|
|
1241
|
+
case "min":
|
|
1242
|
+
return formatAggregateNumber(values.length ? Math.min(...values) : 0);
|
|
1243
|
+
case "max":
|
|
1244
|
+
return formatAggregateNumber(values.length ? Math.max(...values) : 0);
|
|
1245
|
+
default:
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1209
1249
|
function DataGridInner({
|
|
1210
1250
|
columns,
|
|
1211
1251
|
data,
|
|
@@ -1229,6 +1269,13 @@ function DataGridInner({
|
|
|
1229
1269
|
pageSize,
|
|
1230
1270
|
onPageChange,
|
|
1231
1271
|
enableExport = false,
|
|
1272
|
+
groupBy: controlledGroupBy,
|
|
1273
|
+
defaultGroupBy,
|
|
1274
|
+
onGroupByChange,
|
|
1275
|
+
expandedGroups: controlledExpandedGroups,
|
|
1276
|
+
defaultExpandedGroups,
|
|
1277
|
+
onExpandedGroupsChange,
|
|
1278
|
+
groupRowHeight,
|
|
1232
1279
|
onRowClick,
|
|
1233
1280
|
onRowDoubleClick,
|
|
1234
1281
|
onCellClick
|
|
@@ -1248,10 +1295,31 @@ function DataGridInner({
|
|
|
1248
1295
|
const [scrollTop, setScrollTop] = useState8(0);
|
|
1249
1296
|
const [editingRowId, setEditingRowId] = useState8(null);
|
|
1250
1297
|
const [editDraft, setEditDraft] = useState8({});
|
|
1298
|
+
const [uncontrolledGroupBy, setUncontrolledGroupBy] = useState8(defaultGroupBy);
|
|
1299
|
+
const [uncontrolledExpandedGroups, setUncontrolledExpandedGroups] = useState8(defaultExpandedGroups);
|
|
1251
1300
|
const editingEnabled = Boolean(onRowEdit);
|
|
1252
1301
|
const sort = controlledSort !== void 0 ? controlledSort : uncontrolledSort;
|
|
1253
1302
|
const filters = controlledFilters !== void 0 ? controlledFilters : uncontrolledFilters;
|
|
1254
1303
|
const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
|
|
1304
|
+
const groupBy = controlledGroupBy !== void 0 ? controlledGroupBy : uncontrolledGroupBy;
|
|
1305
|
+
const expandedGroups = controlledExpandedGroups !== void 0 ? controlledExpandedGroups : uncontrolledExpandedGroups;
|
|
1306
|
+
const setExpandedGroups = (next) => {
|
|
1307
|
+
if (controlledExpandedGroups === void 0) {
|
|
1308
|
+
setUncontrolledExpandedGroups(next);
|
|
1309
|
+
}
|
|
1310
|
+
onExpandedGroupsChange?.(next);
|
|
1311
|
+
};
|
|
1312
|
+
const isGroupExpanded = (key) => expandedGroups === void 0 ? true : expandedGroups.has(key);
|
|
1313
|
+
const toggleGroupExpanded = (key, allGroupKeys) => {
|
|
1314
|
+
const current2 = expandedGroups ?? new Set(allGroupKeys);
|
|
1315
|
+
const next = new Set(current2);
|
|
1316
|
+
if (next.has(key)) {
|
|
1317
|
+
next.delete(key);
|
|
1318
|
+
} else {
|
|
1319
|
+
next.add(key);
|
|
1320
|
+
}
|
|
1321
|
+
setExpandedGroups(next);
|
|
1322
|
+
};
|
|
1255
1323
|
const setSort = (next) => {
|
|
1256
1324
|
if (controlledSort === void 0) {
|
|
1257
1325
|
setUncontrolledSort(next);
|
|
@@ -1315,6 +1383,44 @@ function DataGridInner({
|
|
|
1315
1383
|
);
|
|
1316
1384
|
return sort.direction === "desc" ? sorted.reverse() : sorted;
|
|
1317
1385
|
}, [filteredData, sort, columns, serverSide, data]);
|
|
1386
|
+
const groupColumn = useMemo2(
|
|
1387
|
+
() => groupBy ? columns.find((c) => c.key === groupBy) : void 0,
|
|
1388
|
+
[columns, groupBy]
|
|
1389
|
+
);
|
|
1390
|
+
const groupedMap = useMemo2(() => {
|
|
1391
|
+
if (!groupBy) return null;
|
|
1392
|
+
const map = /* @__PURE__ */ new Map();
|
|
1393
|
+
for (const row3 of sortedData) {
|
|
1394
|
+
const key = getGroupValue(row3, groupBy, groupColumn);
|
|
1395
|
+
const existing = map.get(key);
|
|
1396
|
+
if (existing) {
|
|
1397
|
+
existing.push(row3);
|
|
1398
|
+
} else {
|
|
1399
|
+
map.set(key, [row3]);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return map;
|
|
1403
|
+
}, [sortedData, groupBy, groupColumn]);
|
|
1404
|
+
const flatEntries = useMemo2(() => {
|
|
1405
|
+
if (!groupedMap) {
|
|
1406
|
+
return sortedData.map((row3) => ({ type: "data", row: row3 }));
|
|
1407
|
+
}
|
|
1408
|
+
const entries = [];
|
|
1409
|
+
for (const [key, rows] of groupedMap) {
|
|
1410
|
+
const collapsed = !isGroupExpanded(key);
|
|
1411
|
+
entries.push({ type: "group", key, rows, collapsed });
|
|
1412
|
+
if (!collapsed) {
|
|
1413
|
+
for (const row3 of rows) {
|
|
1414
|
+
entries.push({ type: "data", row: row3 });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return entries;
|
|
1419
|
+
}, [groupedMap, expandedGroups, sortedData]);
|
|
1420
|
+
const visibleDataRows = useMemo2(
|
|
1421
|
+
() => flatEntries.filter((e) => e.type === "data").map((e) => e.row),
|
|
1422
|
+
[flatEntries]
|
|
1423
|
+
);
|
|
1318
1424
|
const exportToExcel = useCallback2(
|
|
1319
1425
|
async (options) => {
|
|
1320
1426
|
const XLSX = await import("xlsx");
|
|
@@ -1348,13 +1454,13 @@ function DataGridInner({
|
|
|
1348
1454
|
}, [columns, data]);
|
|
1349
1455
|
const hasFilterableColumns = columns.some((c) => c.filterable);
|
|
1350
1456
|
const showPagination = page !== void 0 && pageSize !== void 0 && onPageChange !== void 0;
|
|
1351
|
-
const allSelected =
|
|
1352
|
-
const someSelected = !allSelected &&
|
|
1457
|
+
const allSelected = visibleDataRows.length > 0 && visibleDataRows.every((r) => selectedIds.has(getRowId(r)));
|
|
1458
|
+
const someSelected = !allSelected && visibleDataRows.some((r) => selectedIds.has(getRowId(r)));
|
|
1353
1459
|
const toggleAll = () => {
|
|
1354
1460
|
if (allSelected) {
|
|
1355
1461
|
setSelectedIds(/* @__PURE__ */ new Set());
|
|
1356
1462
|
} else {
|
|
1357
|
-
setSelectedIds(new Set(
|
|
1463
|
+
setSelectedIds(new Set(visibleDataRows.map(getRowId)));
|
|
1358
1464
|
}
|
|
1359
1465
|
};
|
|
1360
1466
|
const toggleRow = (id) => {
|
|
@@ -1479,11 +1585,47 @@ function DataGridInner({
|
|
|
1479
1585
|
setEditingRowId(null);
|
|
1480
1586
|
setEditDraft({});
|
|
1481
1587
|
};
|
|
1482
|
-
const
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1588
|
+
const resolvedGroupRowHeight = groupRowHeight ?? rowHeight;
|
|
1589
|
+
const rowOffsets = groupedMap ? (() => {
|
|
1590
|
+
const offsets = new Array(flatEntries.length);
|
|
1591
|
+
let acc = 0;
|
|
1592
|
+
for (let i = 0; i < flatEntries.length; i++) {
|
|
1593
|
+
offsets[i] = acc;
|
|
1594
|
+
acc += flatEntries[i].type === "group" ? resolvedGroupRowHeight : rowHeight;
|
|
1595
|
+
}
|
|
1596
|
+
return offsets;
|
|
1597
|
+
})() : null;
|
|
1598
|
+
function findIndexAtOffset(offsets, target) {
|
|
1599
|
+
let lo = 0;
|
|
1600
|
+
let hi = offsets.length - 1;
|
|
1601
|
+
let ans = 0;
|
|
1602
|
+
while (lo <= hi) {
|
|
1603
|
+
const mid = lo + hi >> 1;
|
|
1604
|
+
if (offsets[mid] <= target) {
|
|
1605
|
+
ans = mid;
|
|
1606
|
+
lo = mid + 1;
|
|
1607
|
+
} else {
|
|
1608
|
+
hi = mid - 1;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return ans;
|
|
1612
|
+
}
|
|
1613
|
+
let totalHeight;
|
|
1614
|
+
let startIndex;
|
|
1615
|
+
let endIndex;
|
|
1616
|
+
if (rowOffsets) {
|
|
1617
|
+
totalHeight = rowOffsets.length ? rowOffsets[rowOffsets.length - 1] + (flatEntries[flatEntries.length - 1].type === "group" ? resolvedGroupRowHeight : rowHeight) : 0;
|
|
1618
|
+
const rawStart = rowOffsets.length ? findIndexAtOffset(rowOffsets, scrollTop) : 0;
|
|
1619
|
+
startIndex = Math.max(0, rawStart - overscan);
|
|
1620
|
+
const rawEnd = rowOffsets.length ? findIndexAtOffset(rowOffsets, scrollTop + height) + 1 : 0;
|
|
1621
|
+
endIndex = Math.min(flatEntries.length, rawEnd + overscan);
|
|
1622
|
+
} else {
|
|
1623
|
+
totalHeight = sortedData.length * rowHeight;
|
|
1624
|
+
const visibleCount = Math.ceil(height / rowHeight) + overscan * 2;
|
|
1625
|
+
startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
|
|
1626
|
+
endIndex = Math.min(flatEntries.length, startIndex + visibleCount);
|
|
1627
|
+
}
|
|
1628
|
+
const visibleEntries = flatEntries.slice(startIndex, endIndex);
|
|
1487
1629
|
const totalWidth = (selectable ? CHECKBOX_COLUMN_WIDTH : 0) + (editingEnabled ? ACTIONS_COLUMN_WIDTH : 0) + columns.reduce((sum, c) => sum + widthOf(c), 0);
|
|
1488
1630
|
return /* @__PURE__ */ jsxs17(Fragment5, { children: [
|
|
1489
1631
|
/* @__PURE__ */ jsxs17("div", { className: gridWrapper, children: [
|
|
@@ -1657,13 +1799,64 @@ function DataGridInner({
|
|
|
1657
1799
|
width: totalWidth,
|
|
1658
1800
|
minWidth: "100%"
|
|
1659
1801
|
},
|
|
1660
|
-
children:
|
|
1802
|
+
children: visibleEntries.map((entry, i) => {
|
|
1661
1803
|
const index = startIndex + i;
|
|
1804
|
+
const top = rowOffsets ? rowOffsets[index] : index * rowHeight;
|
|
1805
|
+
if (entry.type === "group") {
|
|
1806
|
+
const isExpanded = !entry.collapsed;
|
|
1807
|
+
const groupLabelText = groupColumn ? typeof groupColumn.header === "string" ? groupColumn.header : groupColumn.key : groupBy;
|
|
1808
|
+
const aggregateColumns = columns.filter((c) => c.aggregate);
|
|
1809
|
+
return /* @__PURE__ */ jsxs17(
|
|
1810
|
+
"div",
|
|
1811
|
+
{
|
|
1812
|
+
className: groupRow,
|
|
1813
|
+
style: {
|
|
1814
|
+
top,
|
|
1815
|
+
height: resolvedGroupRowHeight,
|
|
1816
|
+
width: totalWidth
|
|
1817
|
+
},
|
|
1818
|
+
role: "row",
|
|
1819
|
+
children: [
|
|
1820
|
+
/* @__PURE__ */ jsx34(
|
|
1821
|
+
"button",
|
|
1822
|
+
{
|
|
1823
|
+
type: "button",
|
|
1824
|
+
className: groupChevron,
|
|
1825
|
+
"data-expanded": isExpanded || void 0,
|
|
1826
|
+
onClick: () => toggleGroupExpanded(
|
|
1827
|
+
entry.key,
|
|
1828
|
+
Array.from(groupedMap.keys())
|
|
1829
|
+
),
|
|
1830
|
+
"aria-label": isExpanded ? `Collapse group ${entry.key}` : `Expand group ${entry.key}`,
|
|
1831
|
+
children: "\u203A"
|
|
1832
|
+
}
|
|
1833
|
+
),
|
|
1834
|
+
/* @__PURE__ */ jsxs17("span", { className: groupLabel, children: [
|
|
1835
|
+
groupLabelText,
|
|
1836
|
+
": ",
|
|
1837
|
+
entry.key
|
|
1838
|
+
] }),
|
|
1839
|
+
/* @__PURE__ */ jsxs17("span", { className: groupCount, children: [
|
|
1840
|
+
entry.rows.length,
|
|
1841
|
+
" row",
|
|
1842
|
+
entry.rows.length === 1 ? "" : "s"
|
|
1843
|
+
] }),
|
|
1844
|
+
aggregateColumns.length > 0 ? /* @__PURE__ */ jsx34("span", { className: groupAggregates, children: aggregateColumns.map((c) => /* @__PURE__ */ jsxs17("span", { className: groupAggregateItem, children: [
|
|
1845
|
+
typeof c.header === "string" ? c.header : c.key,
|
|
1846
|
+
": ",
|
|
1847
|
+
computeAggregate(c, entry.rows)
|
|
1848
|
+
] }, c.key)) }) : null
|
|
1849
|
+
]
|
|
1850
|
+
},
|
|
1851
|
+
`group-${entry.key}`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const rowData = entry.row;
|
|
1662
1855
|
const id = getRowId(rowData);
|
|
1663
1856
|
const isSelected = selectedIds.has(id);
|
|
1664
1857
|
const isEditing = editingRowId === id;
|
|
1665
1858
|
const style = {
|
|
1666
|
-
top
|
|
1859
|
+
top,
|
|
1667
1860
|
height: rowHeight,
|
|
1668
1861
|
width: totalWidth
|
|
1669
1862
|
};
|
|
@@ -3053,14 +3246,17 @@ import {
|
|
|
3053
3246
|
var chip = "Combobox_chip__26nhic2";
|
|
3054
3247
|
var chipRemove = "Combobox_chipRemove__26nhic3";
|
|
3055
3248
|
var chipsWrapper = "Combobox_chipsWrapper__26nhic1";
|
|
3056
|
-
var emptyState2 = "
|
|
3249
|
+
var emptyState2 = "Combobox_emptyState__26nhic8";
|
|
3057
3250
|
var inputEl = "Combobox_inputEl__26nhic4";
|
|
3058
3251
|
var listbox = "Combobox_listbox__26nhic5";
|
|
3059
3252
|
var option = "Combobox_option__26nhic6";
|
|
3253
|
+
var virtualSpacer = "Combobox_virtualSpacer__26nhic7";
|
|
3060
3254
|
var wrapper9 = "Combobox_wrapper__26nhic0";
|
|
3061
3255
|
|
|
3062
3256
|
// src/components/Combobox/Combobox.tsx
|
|
3063
3257
|
import { jsx as jsx40, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
3258
|
+
var DEFAULT_OPTION_HEIGHT = 37;
|
|
3259
|
+
var OVERSCAN = 5;
|
|
3064
3260
|
function defaultFilter(options, inputText) {
|
|
3065
3261
|
const query = inputText.trim().toLowerCase();
|
|
3066
3262
|
if (!query) return options;
|
|
@@ -3083,6 +3279,8 @@ function Combobox({
|
|
|
3083
3279
|
disabled = false,
|
|
3084
3280
|
invalid,
|
|
3085
3281
|
loading = false,
|
|
3282
|
+
virtualizationThreshold = 50,
|
|
3283
|
+
optionHeight = DEFAULT_OPTION_HEIGHT,
|
|
3086
3284
|
id,
|
|
3087
3285
|
className,
|
|
3088
3286
|
...rest
|
|
@@ -3109,8 +3307,37 @@ function Combobox({
|
|
|
3109
3307
|
}
|
|
3110
3308
|
}, [singleValue, multiple]);
|
|
3111
3309
|
const inputRef = useRef9(null);
|
|
3310
|
+
const listboxRef = useRef9(null);
|
|
3112
3311
|
const listboxId = useId2();
|
|
3312
|
+
const [scrollTop, setScrollTop] = useState14(0);
|
|
3313
|
+
const [viewportHeight, setViewportHeight] = useState14(280);
|
|
3113
3314
|
const filteredOptions = filterOptions(options, multiple ? inputText : open ? inputText : "");
|
|
3315
|
+
const isVirtualized = filteredOptions.length >= virtualizationThreshold;
|
|
3316
|
+
useEffect7(() => {
|
|
3317
|
+
setScrollTop(0);
|
|
3318
|
+
setActiveIndex(null);
|
|
3319
|
+
if (listboxRef.current) listboxRef.current.scrollTop = 0;
|
|
3320
|
+
}, [options]);
|
|
3321
|
+
useEffect7(() => {
|
|
3322
|
+
if (!open) return;
|
|
3323
|
+
const node = listboxRef.current;
|
|
3324
|
+
if (!node) return;
|
|
3325
|
+
if (node.clientHeight > 0) setViewportHeight(node.clientHeight);
|
|
3326
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
3327
|
+
const observer = new ResizeObserver((entries) => {
|
|
3328
|
+
const entry = entries[0];
|
|
3329
|
+
if (entry) setViewportHeight(entry.contentRect.height);
|
|
3330
|
+
});
|
|
3331
|
+
observer.observe(node);
|
|
3332
|
+
return () => observer.disconnect();
|
|
3333
|
+
}, [open]);
|
|
3334
|
+
const totalHeight = filteredOptions.length * optionHeight;
|
|
3335
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
3336
|
+
const effectiveScrollTop = Math.min(scrollTop, maxScrollTop);
|
|
3337
|
+
const visibleCount = Math.ceil(viewportHeight / optionHeight) + OVERSCAN * 2;
|
|
3338
|
+
const startIndex = isVirtualized ? Math.max(0, Math.floor(effectiveScrollTop / optionHeight) - OVERSCAN) : 0;
|
|
3339
|
+
const endIndex = isVirtualized ? Math.min(filteredOptions.length, startIndex + visibleCount) : filteredOptions.length;
|
|
3340
|
+
const visibleOptions = filteredOptions.slice(startIndex, endIndex);
|
|
3114
3341
|
const { refs, floatingStyles, context } = useFloating7({
|
|
3115
3342
|
open,
|
|
3116
3343
|
onOpenChange: setOpen,
|
|
@@ -3155,6 +3382,21 @@ function Combobox({
|
|
|
3155
3382
|
setOpen(true);
|
|
3156
3383
|
setActiveIndex(null);
|
|
3157
3384
|
};
|
|
3385
|
+
const scrollIndexIntoView = (index) => {
|
|
3386
|
+
if (!isVirtualized) return;
|
|
3387
|
+
const rowTop = index * optionHeight;
|
|
3388
|
+
const rowBottom = rowTop + optionHeight;
|
|
3389
|
+
let next = scrollTop;
|
|
3390
|
+
if (rowTop < scrollTop) {
|
|
3391
|
+
next = rowTop;
|
|
3392
|
+
} else if (rowBottom > scrollTop + viewportHeight) {
|
|
3393
|
+
next = rowBottom - viewportHeight;
|
|
3394
|
+
} else {
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
setScrollTop(next);
|
|
3398
|
+
if (listboxRef.current) listboxRef.current.scrollTop = next;
|
|
3399
|
+
};
|
|
3158
3400
|
const moveActiveIndex = (direction2) => {
|
|
3159
3401
|
if (filteredOptions.length === 0) return;
|
|
3160
3402
|
let next = activeIndex ?? (direction2 === 1 ? -1 : filteredOptions.length);
|
|
@@ -3162,6 +3404,7 @@ function Combobox({
|
|
|
3162
3404
|
next = (next + direction2 + filteredOptions.length) % filteredOptions.length;
|
|
3163
3405
|
if (!filteredOptions[next]?.disabled) {
|
|
3164
3406
|
setActiveIndex(next);
|
|
3407
|
+
scrollIndexIntoView(next);
|
|
3165
3408
|
return;
|
|
3166
3409
|
}
|
|
3167
3410
|
}
|
|
@@ -3255,14 +3498,40 @@ function Combobox({
|
|
|
3255
3498
|
open ? /* @__PURE__ */ jsx40(FloatingPortal7, { children: /* @__PURE__ */ jsx40(
|
|
3256
3499
|
"div",
|
|
3257
3500
|
{
|
|
3258
|
-
ref:
|
|
3501
|
+
ref: (node) => {
|
|
3502
|
+
refs.setFloating(node);
|
|
3503
|
+
listboxRef.current = node;
|
|
3504
|
+
},
|
|
3259
3505
|
id: listboxId,
|
|
3260
3506
|
role: "listbox",
|
|
3261
3507
|
"aria-multiselectable": multiple || void 0,
|
|
3262
3508
|
className: listbox,
|
|
3263
3509
|
style: floatingStyles,
|
|
3510
|
+
onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
|
|
3264
3511
|
...getFloatingProps(),
|
|
3265
|
-
children: loading ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: "Loading\u2026" }) : filteredOptions.length === 0 ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: noOptionsMessage }) :
|
|
3512
|
+
children: loading ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: "Loading\u2026" }) : filteredOptions.length === 0 ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: noOptionsMessage }) : isVirtualized ? /* @__PURE__ */ jsx40("div", { className: virtualSpacer, style: { height: totalHeight }, children: visibleOptions.map((opt, i) => {
|
|
3513
|
+
const index = startIndex + i;
|
|
3514
|
+
const isSelected = multiple ? selectedValues.includes(opt.value) : opt.value === singleValue;
|
|
3515
|
+
return /* @__PURE__ */ jsx40(
|
|
3516
|
+
"div",
|
|
3517
|
+
{
|
|
3518
|
+
id: `${listboxId}-option-${index}`,
|
|
3519
|
+
role: "option",
|
|
3520
|
+
"aria-selected": isSelected,
|
|
3521
|
+
"aria-disabled": opt.disabled || void 0,
|
|
3522
|
+
"data-active": activeIndex === index || void 0,
|
|
3523
|
+
className: option,
|
|
3524
|
+
style: { position: "absolute", top: index * optionHeight, left: 0, right: 0, height: optionHeight },
|
|
3525
|
+
onMouseDown: (event) => {
|
|
3526
|
+
event.preventDefault();
|
|
3527
|
+
selectOption(opt);
|
|
3528
|
+
},
|
|
3529
|
+
onMouseEnter: () => setActiveIndex(index),
|
|
3530
|
+
children: opt.label
|
|
3531
|
+
},
|
|
3532
|
+
opt.value
|
|
3533
|
+
);
|
|
3534
|
+
}) }) : filteredOptions.map((opt, index) => {
|
|
3266
3535
|
const isSelected = multiple ? selectedValues.includes(opt.value) : opt.value === singleValue;
|
|
3267
3536
|
return /* @__PURE__ */ jsx40(
|
|
3268
3537
|
"div",
|