@zvndev/yable-react 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { canCellEnterEditMode, functionalUpdate, createTable, getFirstKeyboardCell, getResolvedFocusedCell, detectCellChanges } from '@zvndev/yable-core';
2
- export { CommitError, aggregationFns, createColumnHelper, createLocale, en, filterFns, functionalUpdate, getDefaultLocale, resetLocale, setDefaultLocale, sortingFns } from '@zvndev/yable-core';
1
+ import { canCellEnterEditMode, functionalUpdate, createTable, getDefaultLocale, getFirstKeyboardCell, getResolvedFocusedCell, detectCellChanges } from '@zvndev/yable-core';
2
+ export { CommitError, FormulaEngine, FormulaError, PivotEngine, UndoStack, aggregationFns, createColumnHelper, createLocale, createUndoRedoIntegration, en, filterFns, formulaFunctions, functionalUpdate, generatePivotColumnDefs, getDefaultLocale, getInitialPivotState, getPivotRowModel, resetLocale, setDefaultLocale, sortingFns } from '@zvndev/yable-core';
3
3
  import React3, { createContext, useCallback, useMemo, useState, useRef, useEffect, useContext } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { getInitialRowDragState, moveRow } from '@zvndev/yable-core/features/rowDragging';
@@ -64,6 +64,8 @@ function useTable(options) {
64
64
  prevOptionsRef.current = options;
65
65
  return options;
66
66
  }, [options]);
67
+ const onStateChangeRef = useRef(options.onStateChange);
68
+ onStateChangeRef.current = options.onStateChange;
67
69
  const resolvedState = useMemo(
68
70
  () => ({
69
71
  ...state,
@@ -73,13 +75,14 @@ function useTable(options) {
73
75
  );
74
76
  const onStateChange = useCallback(
75
77
  (updater) => {
76
- if (stableOptions.onStateChange) {
77
- stableOptions.onStateChange(updater);
78
+ const latest = onStateChangeRef.current;
79
+ if (latest) {
80
+ latest(updater);
78
81
  } else {
79
82
  setState((prev) => functionalUpdate(updater, prev));
80
83
  }
81
84
  },
82
- [stableOptions.onStateChange]
85
+ []
83
86
  );
84
87
  const resolvedOptions = useMemo(
85
88
  () => ({
@@ -207,7 +210,15 @@ function useVirtualization({
207
210
  container.scrollTop = offset;
208
211
  }
209
212
  },
210
- [containerRef, totalRows, rowHeight, isFixedHeight, getRowHeight, hasPretextHeights, pretextPrefixSums]
213
+ [
214
+ containerRef,
215
+ totalRows,
216
+ rowHeight,
217
+ isFixedHeight,
218
+ getRowHeight,
219
+ hasPretextHeights,
220
+ pretextPrefixSums
221
+ ]
211
222
  );
212
223
  const totalHeight = useMemo(() => {
213
224
  if (totalRows === 0) return 0;
@@ -261,10 +272,7 @@ function useVirtualization({
261
272
  } else if (isFixedHeight) {
262
273
  const fixedH = rowHeight;
263
274
  startIndex = Math.floor(scrollTop / fixedH);
264
- endIndex = Math.min(
265
- totalRows - 1,
266
- Math.ceil((scrollTop + containerHeight) / fixedH) - 1
267
- );
275
+ endIndex = Math.min(totalRows - 1, Math.ceil((scrollTop + containerHeight) / fixedH) - 1);
268
276
  } else {
269
277
  let accum = 0;
270
278
  let foundStart = false;
@@ -331,6 +339,162 @@ function useVirtualization({
331
339
  scrollTo
332
340
  };
333
341
  }
342
+ function binarySearchOffsets(offsets, target) {
343
+ let low = 0;
344
+ let high = offsets.length - 1;
345
+ while (low < high) {
346
+ const mid = low + high >>> 1;
347
+ if (offsets[mid + 1] <= target) {
348
+ low = mid + 1;
349
+ } else {
350
+ high = mid;
351
+ }
352
+ }
353
+ return low;
354
+ }
355
+ function useColumnVirtualization({
356
+ containerRef,
357
+ columns,
358
+ overscan = 2,
359
+ enabled = true
360
+ }) {
361
+ const [scrollState, setScrollState] = useState({
362
+ scrollLeft: 0,
363
+ containerWidth: 0
364
+ });
365
+ const rafRef = useRef(null);
366
+ const sizes = useMemo(() => columns.map((column) => column.getSize()), [columns]);
367
+ const offsets = useMemo(() => {
368
+ const next = new Array(columns.length + 1);
369
+ next[0] = 0;
370
+ for (let i = 0; i < columns.length; i++) {
371
+ next[i + 1] = next[i] + (sizes[i] ?? 0);
372
+ }
373
+ return next;
374
+ }, [columns.length, sizes]);
375
+ const totalWidth = offsets[offsets.length - 1] ?? 0;
376
+ useEffect(() => {
377
+ const container = containerRef.current;
378
+ if (!container) return;
379
+ setScrollState({
380
+ scrollLeft: container.scrollLeft,
381
+ containerWidth: container.clientWidth
382
+ });
383
+ const handleScroll = () => {
384
+ if (rafRef.current !== null) return;
385
+ rafRef.current = requestAnimationFrame(() => {
386
+ rafRef.current = null;
387
+ const el = containerRef.current;
388
+ if (!el) return;
389
+ setScrollState({
390
+ scrollLeft: el.scrollLeft,
391
+ containerWidth: el.clientWidth
392
+ });
393
+ });
394
+ };
395
+ container.addEventListener("scroll", handleScroll, { passive: true });
396
+ let resizeObserver;
397
+ if (typeof ResizeObserver !== "undefined") {
398
+ resizeObserver = new ResizeObserver(() => {
399
+ const el = containerRef.current;
400
+ if (!el) return;
401
+ setScrollState((prev) => {
402
+ const nextWidth = el.clientWidth;
403
+ if (prev.containerWidth === nextWidth && prev.scrollLeft === el.scrollLeft) {
404
+ return prev;
405
+ }
406
+ return {
407
+ scrollLeft: el.scrollLeft,
408
+ containerWidth: nextWidth
409
+ };
410
+ });
411
+ });
412
+ resizeObserver.observe(container);
413
+ }
414
+ return () => {
415
+ container.removeEventListener("scroll", handleScroll);
416
+ if (rafRef.current !== null) {
417
+ cancelAnimationFrame(rafRef.current);
418
+ rafRef.current = null;
419
+ }
420
+ resizeObserver?.disconnect();
421
+ };
422
+ }, [containerRef]);
423
+ const scrollToIndex = useCallback(
424
+ (index) => {
425
+ const container = containerRef.current;
426
+ if (!container || columns.length === 0) return;
427
+ const clampedIndex = Math.max(0, Math.min(index, columns.length - 1));
428
+ container.scrollLeft = offsets[clampedIndex] ?? 0;
429
+ },
430
+ [columns.length, containerRef, offsets]
431
+ );
432
+ if (!enabled || columns.length === 0) {
433
+ return {
434
+ virtualColumns: columns.map((column, index) => ({
435
+ column,
436
+ index,
437
+ start: offsets[index] ?? 0,
438
+ size: sizes[index] ?? 0
439
+ })),
440
+ startOffset: 0,
441
+ endOffset: 0,
442
+ totalWidth,
443
+ visibleWidth: totalWidth,
444
+ startIndex: 0,
445
+ endIndex: Math.max(columns.length - 1, 0),
446
+ isVirtualized: false,
447
+ scrollToIndex
448
+ };
449
+ }
450
+ const { scrollLeft, containerWidth } = scrollState;
451
+ if (containerWidth <= 0 || totalWidth <= containerWidth) {
452
+ return {
453
+ virtualColumns: columns.map((column, index) => ({
454
+ column,
455
+ index,
456
+ start: offsets[index] ?? 0,
457
+ size: sizes[index] ?? 0
458
+ })),
459
+ startOffset: 0,
460
+ endOffset: 0,
461
+ totalWidth,
462
+ visibleWidth: totalWidth,
463
+ startIndex: 0,
464
+ endIndex: Math.max(columns.length - 1, 0),
465
+ isVirtualized: false,
466
+ scrollToIndex
467
+ };
468
+ }
469
+ const startIndex = binarySearchOffsets(offsets, scrollLeft);
470
+ const endBoundary = scrollLeft + containerWidth;
471
+ const endIndex = binarySearchOffsets(offsets, Math.max(scrollLeft, endBoundary - 1));
472
+ const overscanStart = Math.max(0, startIndex - overscan);
473
+ const overscanEnd = Math.min(columns.length - 1, endIndex + overscan);
474
+ const virtualColumns = [];
475
+ for (let i = overscanStart; i <= overscanEnd; i++) {
476
+ virtualColumns.push({
477
+ column: columns[i],
478
+ index: i,
479
+ start: offsets[i] ?? 0,
480
+ size: sizes[i] ?? 0
481
+ });
482
+ }
483
+ const startOffset = offsets[overscanStart] ?? 0;
484
+ const visibleWidth = (offsets[overscanEnd + 1] ?? totalWidth) - startOffset;
485
+ const endOffset = totalWidth - (offsets[overscanEnd + 1] ?? totalWidth);
486
+ return {
487
+ virtualColumns,
488
+ startOffset,
489
+ endOffset,
490
+ totalWidth,
491
+ visibleWidth,
492
+ startIndex: overscanStart,
493
+ endIndex: overscanEnd,
494
+ isVirtualized: true,
495
+ scrollToIndex
496
+ };
497
+ }
334
498
  var pretextPromise = null;
335
499
  function loadPretext() {
336
500
  if (pretextPromise) return pretextPromise;
@@ -381,7 +545,12 @@ function usePretextMeasurement({
381
545
  }
382
546
  prepareTimeRef.current = performance.now() - start;
383
547
  return result;
384
- }, [pretext, enabled, data, columns.map((c) => `${c.columnId}:${c.font}:${c.fixedHeight ? "F" : "T"}`).join("|")]);
548
+ }, [
549
+ pretext,
550
+ enabled,
551
+ data,
552
+ columns.map((c) => `${c.columnId}:${c.font}:${c.fixedHeight ? "F" : "T"}`).join("|")
553
+ ]);
385
554
  const measurement = useMemo(() => {
386
555
  if (!pretext || !preparedCells) {
387
556
  return { rowHeights: null, prefixSums: null, totalHeight: 0 };
@@ -419,6 +588,7 @@ function usePretextMeasurement({
419
588
  return {
420
589
  rowHeights,
421
590
  prefixSums,
591
+ // Safe: prefixSums has length data.length + 1, so [data.length] always exists
422
592
  totalHeight: prefixSums[data.length]
423
593
  };
424
594
  }, [pretext, preparedCells, columnWidthsKey, minRowHeight, data.length]);
@@ -683,18 +853,10 @@ function CellDate({
683
853
  if (raw == null) return null;
684
854
  const date = raw instanceof Date ? raw : new Date(String(raw));
685
855
  if (isNaN(date.getTime())) return /* @__PURE__ */ jsx("span", { className: "yable-cell-date", children: String(raw) });
686
- const formatter = useMemo(() => {
687
- if (typeof format === "string" && format !== "relative") {
688
- return new Intl.DateTimeFormat(locale, {
689
- ...PRESETS[format],
690
- timeZone: "UTC"
691
- });
692
- }
693
- if (typeof format === "object") {
694
- return new Intl.DateTimeFormat(locale, { ...format, timeZone: "UTC" });
695
- }
696
- return null;
697
- }, [format, locale]);
856
+ const formatter = typeof format === "string" && format !== "relative" ? new Intl.DateTimeFormat(locale, {
857
+ ...PRESETS[format],
858
+ timeZone: "UTC"
859
+ }) : typeof format === "object" ? new Intl.DateTimeFormat(locale, { ...format, timeZone: "UTC" }) : null;
698
860
  const formatted = format === "relative" ? formatRelative(date) : formatter ? formatter.format(date) : date.toLocaleDateString(locale);
699
861
  return /* @__PURE__ */ jsx("span", { className: `yable-cell-date ${className ?? ""}`, title: date.toISOString(), children: formatted });
700
862
  }
@@ -705,8 +867,14 @@ var measureRecipe9 = {
705
867
  padding: 20
706
868
  };
707
869
  function isSafeUrl(url) {
708
- const normalized = String(url).toLowerCase().trim();
709
- return !normalized.startsWith("javascript:") && !normalized.startsWith("data:text/html") && !normalized.startsWith("vbscript:");
870
+ const normalized = String(url).trim();
871
+ if (/^[/#?]/.test(normalized)) return true;
872
+ try {
873
+ const parsed = new URL(normalized, "https://placeholder.invalid");
874
+ return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
875
+ } catch {
876
+ return false;
877
+ }
710
878
  }
711
879
  function CellLink({
712
880
  context,
@@ -883,7 +1051,7 @@ function useTableContext() {
883
1051
  const ctx = useContext(TableContext);
884
1052
  if (!ctx) {
885
1053
  throw new Error(
886
- "[yable] useTableContext must be used within a <Table> component. Did you forget to pass the `table` prop?"
1054
+ "[yable E001] useTableContext must be used within a <Table> component. Did you forget to pass the `table` prop?"
887
1055
  );
888
1056
  }
889
1057
  return ctx;
@@ -933,12 +1101,234 @@ function SortIndicator({ direction, index }) {
933
1101
  }
934
1102
  );
935
1103
  }
1104
+ function formatValue(value) {
1105
+ if (value == null || value === "") return "(empty)";
1106
+ if (typeof value === "string") return value;
1107
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1108
+ if (value instanceof Date) return value.toISOString();
1109
+ return JSON.stringify(value);
1110
+ }
1111
+ function SetFilter({ column, className }) {
1112
+ const [open, setOpen] = useState(false);
1113
+ const filterValue = column.getFilterValue();
1114
+ const selectedValues = Array.isArray(filterValue) ? filterValue : filterValue == null || filterValue === "" ? [] : [filterValue];
1115
+ const facetedUniqueValues = column.getFacetedUniqueValues();
1116
+ const options = Array.from(facetedUniqueValues.entries()).map(([value, count]) => ({
1117
+ value,
1118
+ count,
1119
+ label: formatValue(value)
1120
+ })).sort((a, b) => a.label.localeCompare(b.label));
1121
+ const selectedSet = new Set(selectedValues);
1122
+ const toggleValue = (value) => {
1123
+ const next = new Set(selectedSet);
1124
+ if (next.has(value)) {
1125
+ next.delete(value);
1126
+ } else {
1127
+ next.add(value);
1128
+ }
1129
+ const nextValues = Array.from(next);
1130
+ column.setFilterValue(nextValues.length > 0 ? nextValues : void 0);
1131
+ };
1132
+ const headerLabel = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
1133
+ return /* @__PURE__ */ jsxs(
1134
+ "div",
1135
+ {
1136
+ className: ["yable-set-filter", className].filter(Boolean).join(" "),
1137
+ style: { position: "relative" },
1138
+ children: [
1139
+ /* @__PURE__ */ jsx(
1140
+ "button",
1141
+ {
1142
+ type: "button",
1143
+ className: "yable-set-filter-trigger",
1144
+ "aria-haspopup": "dialog",
1145
+ "aria-expanded": open,
1146
+ onClick: () => setOpen((prev) => !prev),
1147
+ style: {
1148
+ width: "100%",
1149
+ minHeight: 28,
1150
+ padding: "4px 8px",
1151
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1152
+ borderRadius: 6,
1153
+ background: "transparent",
1154
+ font: "inherit",
1155
+ textAlign: "left",
1156
+ cursor: "pointer"
1157
+ },
1158
+ children: selectedValues.length > 0 ? `${selectedValues.length} selected` : `Filter ${headerLabel}`
1159
+ }
1160
+ ),
1161
+ open && /* @__PURE__ */ jsxs(
1162
+ "div",
1163
+ {
1164
+ role: "dialog",
1165
+ "aria-label": `${headerLabel} set filter`,
1166
+ className: "yable-set-filter-popover",
1167
+ style: {
1168
+ position: "absolute",
1169
+ insetInlineStart: 0,
1170
+ top: "calc(100% + 4px)",
1171
+ zIndex: 20,
1172
+ width: 220,
1173
+ maxHeight: 240,
1174
+ overflow: "auto",
1175
+ padding: 8,
1176
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1177
+ borderRadius: 8,
1178
+ background: "var(--yable-color-bg, #fff)",
1179
+ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.12)"
1180
+ },
1181
+ children: [
1182
+ /* @__PURE__ */ jsxs(
1183
+ "div",
1184
+ {
1185
+ style: {
1186
+ display: "flex",
1187
+ alignItems: "center",
1188
+ justifyContent: "space-between",
1189
+ gap: 8,
1190
+ marginBottom: 8
1191
+ },
1192
+ children: [
1193
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 12 }, children: headerLabel }),
1194
+ selectedValues.length > 0 && /* @__PURE__ */ jsx(
1195
+ "button",
1196
+ {
1197
+ type: "button",
1198
+ onClick: () => column.setFilterValue(void 0),
1199
+ style: {
1200
+ border: "none",
1201
+ background: "transparent",
1202
+ padding: 0,
1203
+ cursor: "pointer",
1204
+ fontSize: 12
1205
+ },
1206
+ children: "Clear"
1207
+ }
1208
+ )
1209
+ ]
1210
+ }
1211
+ ),
1212
+ /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": `${headerLabel} options`, children: [
1213
+ options.length === 0 && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, opacity: 0.75 }, children: "No values" }),
1214
+ options.map((option) => {
1215
+ const checked = selectedSet.has(option.value);
1216
+ return /* @__PURE__ */ jsxs(
1217
+ "label",
1218
+ {
1219
+ style: {
1220
+ display: "flex",
1221
+ alignItems: "center",
1222
+ gap: 8,
1223
+ padding: "4px 0",
1224
+ fontSize: 12
1225
+ },
1226
+ children: [
1227
+ /* @__PURE__ */ jsx(
1228
+ "input",
1229
+ {
1230
+ type: "checkbox",
1231
+ checked,
1232
+ onChange: () => toggleValue(option.value)
1233
+ }
1234
+ ),
1235
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: option.label }),
1236
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.6 }, children: option.count })
1237
+ ]
1238
+ },
1239
+ `${option.label}-${option.count}`
1240
+ );
1241
+ })
1242
+ ] })
1243
+ ]
1244
+ }
1245
+ )
1246
+ ]
1247
+ }
1248
+ );
1249
+ }
1250
+ function isSetCompatibleValue(value) {
1251
+ return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1252
+ }
1253
+ function inferFilterVariant(column) {
1254
+ const meta = column.columnDef.meta ?? {};
1255
+ if (meta.filterVariant) return meta.filterVariant;
1256
+ const uniqueValues = column.getFacetedUniqueValues();
1257
+ if (uniqueValues.size > 0 && uniqueValues.size <= 12) {
1258
+ const allValuesSupported = Array.from(uniqueValues.keys()).every(isSetCompatibleValue);
1259
+ if (allValuesSupported) return "set";
1260
+ }
1261
+ return "text";
1262
+ }
1263
+ function FloatingFilter({ column }) {
1264
+ const variant = inferFilterVariant(column);
1265
+ if (!column.getCanFilter()) {
1266
+ return /* @__PURE__ */ jsx("div", { style: { minHeight: 28 }, "aria-hidden": "true" });
1267
+ }
1268
+ if (variant === "set") {
1269
+ return /* @__PURE__ */ jsx(SetFilter, { column });
1270
+ }
1271
+ const filterValue = column.getFilterValue();
1272
+ const normalizedValue = typeof filterValue === "string" || typeof filterValue === "number" ? String(filterValue) : "";
1273
+ const label = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
1274
+ return /* @__PURE__ */ jsx("label", { style: { display: "block" }, children: /* @__PURE__ */ jsx(
1275
+ "input",
1276
+ {
1277
+ "aria-label": `Filter ${label}`,
1278
+ className: "yable-floating-filter-input",
1279
+ value: normalizedValue,
1280
+ onChange: (event) => {
1281
+ const nextValue = event.target.value;
1282
+ column.setFilterValue(nextValue === "" ? void 0 : nextValue);
1283
+ },
1284
+ placeholder: `Filter ${label}`,
1285
+ style: {
1286
+ width: "100%",
1287
+ minHeight: 28,
1288
+ padding: "4px 8px",
1289
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1290
+ borderRadius: 6,
1291
+ background: "transparent",
1292
+ font: "inherit"
1293
+ }
1294
+ }
1295
+ ) });
1296
+ }
936
1297
  var DRAG_MIME = "application/yable-column";
937
1298
  function TableHeader({
938
- table
1299
+ table,
1300
+ floatingFilters = false
939
1301
  }) {
940
1302
  const headerGroups = table.getHeaderGroups();
941
- return /* @__PURE__ */ jsx("thead", { className: "yable-thead", children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)) });
1303
+ const visibleColumns = table.getVisibleLeafColumns();
1304
+ return /* @__PURE__ */ jsxs("thead", { className: "yable-thead", children: [
1305
+ headerGroups.map((headerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)),
1306
+ floatingFilters && visibleColumns.length > 0 && /* @__PURE__ */ jsx("tr", { className: "yable-header-row yable-header-row--filters", children: visibleColumns.map((column) => /* @__PURE__ */ jsx(FloatingFilterCell, { column }, `${column.id}-filter`)) })
1307
+ ] });
1308
+ }
1309
+ function FloatingFilterCell({
1310
+ column
1311
+ }) {
1312
+ const style = useMemo(() => {
1313
+ const next = {
1314
+ width: column.getSize(),
1315
+ minWidth: column.columnDef.minSize,
1316
+ maxWidth: column.columnDef.maxSize,
1317
+ padding: 6,
1318
+ verticalAlign: "top"
1319
+ };
1320
+ const pinned = column.getIsPinned();
1321
+ if (pinned) {
1322
+ next.position = "sticky";
1323
+ if (pinned === "left") {
1324
+ next.left = column.getStart("left");
1325
+ } else {
1326
+ next.right = column.getStart("right");
1327
+ }
1328
+ }
1329
+ return next;
1330
+ }, [column]);
1331
+ return /* @__PURE__ */ jsx("th", { className: "yable-th yable-th--filter", role: "columnheader", style, children: /* @__PURE__ */ jsx(FloatingFilter, { column }) });
942
1332
  }
943
1333
  function HeaderCell({
944
1334
  header,
@@ -1027,14 +1417,11 @@ function HeaderCell({
1027
1417
  },
1028
1418
  [canReorder]
1029
1419
  );
1030
- const handleDragLeave = useCallback(
1031
- (e) => {
1032
- const next = e.relatedTarget;
1033
- if (next && e.currentTarget.contains(next)) return;
1034
- setDragOver(null);
1035
- },
1036
- []
1037
- );
1420
+ const handleDragLeave = useCallback((e) => {
1421
+ const next = e.relatedTarget;
1422
+ if (next && e.currentTarget.contains(next)) return;
1423
+ setDragOver(null);
1424
+ }, []);
1038
1425
  const handleDragEnd = useCallback(() => {
1039
1426
  setDragOver(null);
1040
1427
  }, []);
@@ -1107,13 +1494,7 @@ function HeaderCell({
1107
1494
  children: [
1108
1495
  /* @__PURE__ */ jsxs("div", { className: "yable-th-content", children: [
1109
1496
  /* @__PURE__ */ jsx("span", { children: headerContent }),
1110
- canSort && /* @__PURE__ */ jsx(
1111
- SortIndicator,
1112
- {
1113
- direction: sortDirection,
1114
- index: sortIndex > 0 ? sortIndex : void 0
1115
- }
1116
- )
1497
+ canSort && /* @__PURE__ */ jsx(SortIndicator, { direction: sortDirection, index: sortIndex > 0 ? sortIndex : void 0 })
1117
1498
  ] }),
1118
1499
  canResize && /* @__PURE__ */ jsx(
1119
1500
  "div",
@@ -1238,6 +1619,10 @@ function TableCell({
1238
1619
  const cellStatus = table.getCellStatus(cell.row.id, column.id);
1239
1620
  const cellErrorMessage = table.getCellErrorMessage(cell.row.id, column.id);
1240
1621
  const cellConflictWith = table.getCellConflictWith(cell.row.id, column.id);
1622
+ const selectionRange = table.getCellSelectionRange();
1623
+ const selectionEdges = table.getCellSelectionEdges(rowIndex, columnIndex);
1624
+ const isCellSelected = selectionEdges !== null;
1625
+ const isMultiCellSelection = selectionRange !== null && (selectionRange.start.rowIndex !== selectionRange.end.rowIndex || selectionRange.start.columnIndex !== selectionRange.end.columnIndex);
1241
1626
  const overrideValue = cellStatus !== "idle" ? table.getCellRenderValue(cell.row.id, column.id) : void 0;
1242
1627
  let content;
1243
1628
  const cellDef = column.columnDef.cell;
@@ -1261,23 +1646,17 @@ function TableCell({
1261
1646
  }
1262
1647
  const handleClick = useCallback(
1263
1648
  (e) => {
1264
- table.setFocusedCell({ rowIndex, columnIndex });
1265
- const clickTarget = e.target;
1266
- if (!isInteractiveClickTarget(clickTarget)) {
1267
- const currentTarget = e.currentTarget;
1268
- currentTarget.focus({ preventScroll: true });
1269
- }
1270
1649
  table.events.emit("cell:click", {
1271
1650
  cell,
1272
1651
  row: cell.row,
1273
1652
  column: cell.column,
1274
1653
  event: e.nativeEvent
1275
1654
  });
1276
- if (canCellEnterEditMode(table, cell.row, column) && !isAlwaysEditable && !isEditing) {
1655
+ if (canCellEnterEditMode(table, cell.row, column) && !isAlwaysEditable && !isEditing && !isMultiCellSelection && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
1277
1656
  table.startEditing(cell.row.id, column.id);
1278
1657
  }
1279
1658
  },
1280
- [cell, column, columnIndex, isAlwaysEditable, isEditing, rowIndex, table]
1659
+ [cell, column, isAlwaysEditable, isEditing, isMultiCellSelection, table]
1281
1660
  );
1282
1661
  const handleDoubleClick = useCallback(
1283
1662
  (e) => {
@@ -1305,9 +1684,34 @@ function TableCell({
1305
1684
  if (!keyboardNavigationEnabled) return;
1306
1685
  table.setFocusedCell({ rowIndex, columnIndex });
1307
1686
  }, [columnIndex, keyboardNavigationEnabled, rowIndex, table]);
1687
+ const handleMouseDown = useCallback(
1688
+ (e) => {
1689
+ if (e.button !== 0) return;
1690
+ const clickTarget = e.target;
1691
+ if (isInteractiveClickTarget(clickTarget)) return;
1692
+ e.preventDefault();
1693
+ table.setFocusedCell({ rowIndex, columnIndex });
1694
+ table.startCellRangeSelection({ rowIndex, columnIndex }, { extend: e.shiftKey });
1695
+ e.currentTarget.focus({ preventScroll: true });
1696
+ },
1697
+ [columnIndex, rowIndex, table]
1698
+ );
1699
+ const handleMouseEnter = useCallback(() => {
1700
+ if (!table.getState().cellSelection?.isDragging) return;
1701
+ table.updateCellRangeSelection({ rowIndex, columnIndex });
1702
+ }, [columnIndex, rowIndex, table]);
1703
+ const handleMouseUp = useCallback(() => {
1704
+ if (!table.getState().cellSelection?.isDragging) return;
1705
+ table.endCellRangeSelection();
1706
+ }, [table]);
1308
1707
  const classNames = [
1309
1708
  "yable-td",
1310
- isFocused && "yable-cell--focused"
1709
+ isFocused && "yable-cell--focused",
1710
+ isCellSelected && "yable-cell--selected",
1711
+ selectionEdges?.top && "yable-cell--selection-top",
1712
+ selectionEdges?.right && "yable-cell--selection-right",
1713
+ selectionEdges?.bottom && "yable-cell--selection-bottom",
1714
+ selectionEdges?.left && "yable-cell--selection-left"
1311
1715
  ].filter(Boolean).join(" ");
1312
1716
  return /* @__PURE__ */ jsxs(
1313
1717
  "td",
@@ -1321,10 +1725,14 @@ function TableCell({
1321
1725
  "data-column-id": column.id,
1322
1726
  "data-row-index": rowIndex,
1323
1727
  "data-column-index": columnIndex,
1728
+ "data-cell-selected": isCellSelected || void 0,
1324
1729
  "aria-rowindex": rowIndex + 1,
1325
1730
  "aria-colindex": columnIndex + 1,
1326
1731
  role: "gridcell",
1327
1732
  tabIndex: keyboardNavigationEnabled ? isTabStop ? 0 : -1 : void 0,
1733
+ onMouseDown: handleMouseDown,
1734
+ onMouseEnter: handleMouseEnter,
1735
+ onMouseUp: handleMouseUp,
1328
1736
  onClick: handleClick,
1329
1737
  onDoubleClick: handleDoubleClick,
1330
1738
  onContextMenu: handleContextMenu,
@@ -1449,14 +1857,15 @@ var CellErrorBoundary = class extends React3.Component {
1449
1857
  return this.props.children;
1450
1858
  }
1451
1859
  };
1452
- function TableBody({
1453
- table,
1454
- clickableRows
1455
- }) {
1860
+ function TableBody({ table, clickableRows }) {
1456
1861
  const rows = table.getRowModel().rows;
1457
1862
  const visibleColumns = table.getVisibleLeafColumns();
1458
1863
  const activeCell = table.getState().editing.activeCell;
1459
1864
  const focusedCell = table.getFocusedCell();
1865
+ const cellSelection = table.getState().cellSelection ?? {
1866
+ range: null,
1867
+ isDragging: false
1868
+ };
1460
1869
  const options = table.options;
1461
1870
  const enableVirtualization = options.enableVirtualization ?? false;
1462
1871
  const scrollContainerRef = useRef(null);
@@ -1474,6 +1883,18 @@ function TableBody({
1474
1883
  pretextHeights,
1475
1884
  pretextPrefixSums
1476
1885
  });
1886
+ const cellSelectionKey = cellSelection.range ? `${cellSelection.range.start.rowIndex}:${cellSelection.range.start.columnIndex}:${cellSelection.range.end.rowIndex}:${cellSelection.range.end.columnIndex}:${cellSelection.isDragging ? "dragging" : "idle"}` : `none:${cellSelection.isDragging ? "dragging" : "idle"}`;
1887
+ useEffect(() => {
1888
+ const handleWindowMouseUp = () => {
1889
+ if (table.getState().cellSelection?.isDragging) {
1890
+ table.endCellRangeSelection();
1891
+ }
1892
+ };
1893
+ window.addEventListener("mouseup", handleWindowMouseUp);
1894
+ return () => {
1895
+ window.removeEventListener("mouseup", handleWindowMouseUp);
1896
+ };
1897
+ }, [table]);
1477
1898
  if (!enableVirtualization) {
1478
1899
  return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx(
1479
1900
  MemoizedTableRow,
@@ -1487,6 +1908,7 @@ function TableBody({
1487
1908
  activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1488
1909
  focusedColumnIndex: focusedCell?.rowIndex === rowIndex ? focusedCell.columnIndex : null,
1489
1910
  hasFocusedCell: focusedCell !== null,
1911
+ cellSelectionKey,
1490
1912
  clickable: clickableRows
1491
1913
  },
1492
1914
  row.id
@@ -1495,73 +1917,67 @@ function TableBody({
1495
1917
  const hasPretextData = !!(pretextHeights && pretextPrefixSums);
1496
1918
  const fixedRowHeight = typeof rowHeight === "number" && !hasPretextData ? rowHeight : void 0;
1497
1919
  const containerHeight = hasPretextData ? Math.min(totalHeight, 800) : fixedRowHeight ? Math.min(totalHeight, fixedRowHeight * 20) : 600;
1498
- return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: /* @__PURE__ */ jsx("tr", { style: { height: 0, padding: 0, border: "none" }, children: /* @__PURE__ */ jsx(
1499
- "td",
1920
+ return /* @__PURE__ */ jsx("tbody", { className: "yable-tbody", children: /* @__PURE__ */ jsx("tr", { style: { height: 0, padding: 0, border: "none" }, children: /* @__PURE__ */ jsx("td", { style: { height: 0, padding: 0, border: "none" }, colSpan: visibleColumns.length, children: /* @__PURE__ */ jsx(
1921
+ "div",
1500
1922
  {
1501
- style: { height: 0, padding: 0, border: "none" },
1502
- colSpan: visibleColumns.length,
1923
+ ref: scrollContainerRef,
1924
+ className: "yable-virtual-scroll-container",
1925
+ style: {
1926
+ overflowY: "auto",
1927
+ height: containerHeight,
1928
+ position: "relative"
1929
+ },
1503
1930
  children: /* @__PURE__ */ jsx(
1504
1931
  "div",
1505
1932
  {
1506
- ref: scrollContainerRef,
1507
- className: "yable-virtual-scroll-container",
1508
- style: {
1509
- overflowY: "auto",
1510
- height: containerHeight,
1511
- position: "relative"
1512
- },
1933
+ className: "yable-virtual-spacer",
1934
+ style: { height: totalHeight, position: "relative" },
1513
1935
  children: /* @__PURE__ */ jsx(
1514
- "div",
1936
+ "table",
1515
1937
  {
1516
- className: "yable-virtual-spacer",
1517
- style: { height: totalHeight, position: "relative" },
1518
- children: /* @__PURE__ */ jsx(
1519
- "table",
1520
- {
1521
- style: {
1522
- position: "absolute",
1523
- top: 0,
1524
- left: 0,
1525
- width: "100%",
1526
- tableLayout: "fixed",
1527
- borderCollapse: "collapse"
1938
+ style: {
1939
+ position: "absolute",
1940
+ top: 0,
1941
+ left: 0,
1942
+ width: "100%",
1943
+ tableLayout: "fixed",
1944
+ borderCollapse: "collapse"
1945
+ },
1946
+ children: /* @__PURE__ */ jsx("tbody", { children: virtualRows.map((vRow) => {
1947
+ const row = rows[vRow.index];
1948
+ if (!row) return null;
1949
+ return /* @__PURE__ */ jsx(
1950
+ MemoizedTableRow,
1951
+ {
1952
+ row,
1953
+ table,
1954
+ rowIndex: vRow.index,
1955
+ visibleColumns,
1956
+ isSelected: row.getIsSelected(),
1957
+ isExpanded: row.getIsExpanded(),
1958
+ activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1959
+ focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
1960
+ hasFocusedCell: focusedCell !== null,
1961
+ cellSelectionKey,
1962
+ clickable: clickableRows,
1963
+ virtualStyle: {
1964
+ position: "absolute",
1965
+ top: 0,
1966
+ left: 0,
1967
+ width: "100%",
1968
+ height: vRow.size,
1969
+ transform: `translateY(${vRow.start}px)`
1970
+ }
1528
1971
  },
1529
- children: /* @__PURE__ */ jsx("tbody", { children: virtualRows.map((vRow) => {
1530
- const row = rows[vRow.index];
1531
- if (!row) return null;
1532
- return /* @__PURE__ */ jsx(
1533
- MemoizedTableRow,
1534
- {
1535
- row,
1536
- table,
1537
- rowIndex: vRow.index,
1538
- visibleColumns,
1539
- isSelected: row.getIsSelected(),
1540
- isExpanded: row.getIsExpanded(),
1541
- activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1542
- focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
1543
- hasFocusedCell: focusedCell !== null,
1544
- clickable: clickableRows,
1545
- virtualStyle: {
1546
- position: "absolute",
1547
- top: 0,
1548
- left: 0,
1549
- width: "100%",
1550
- height: vRow.size,
1551
- transform: `translateY(${vRow.start}px)`
1552
- }
1553
- },
1554
- row.id
1555
- );
1556
- }) })
1557
- }
1558
- )
1972
+ row.id
1973
+ );
1974
+ }) })
1559
1975
  }
1560
1976
  )
1561
1977
  }
1562
1978
  )
1563
1979
  }
1564
- ) }) });
1980
+ ) }) }) });
1565
1981
  }
1566
1982
  function TableRowInner({
1567
1983
  row,
@@ -1573,10 +1989,12 @@ function TableRowInner({
1573
1989
  activeColumnId,
1574
1990
  focusedColumnIndex,
1575
1991
  hasFocusedCell,
1992
+ cellSelectionKey: _cellSelectionKey,
1576
1993
  clickable,
1577
1994
  virtualStyle
1578
1995
  }) {
1579
- const visibleCells = row.getVisibleCells();
1996
+ const allCells = row.getAllCells();
1997
+ const visibleCells = visibleColumns.map((column) => allCells.find((cell) => cell.column.id === column.id)).filter((cell) => cell != null);
1580
1998
  const handleClick = useCallback(
1581
1999
  (e) => {
1582
2000
  if (clickable) {
@@ -1606,6 +2024,8 @@ function TableRowInner({
1606
2024
  },
1607
2025
  [table.events, row]
1608
2026
  );
2027
+ const selectionEnabled = Boolean(table.options.enableRowSelection);
2028
+ const expansionEnabled = Boolean(table.options.enableExpanding);
1609
2029
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1610
2030
  /* @__PURE__ */ jsx(
1611
2031
  "tr",
@@ -1617,6 +2037,8 @@ function TableRowInner({
1617
2037
  "data-clickable": clickable || void 0,
1618
2038
  "data-row-id": row.id,
1619
2039
  "data-row-index": rowIndex,
2040
+ "aria-selected": selectionEnabled ? isSelected : void 0,
2041
+ "aria-expanded": expansionEnabled ? isExpanded : void 0,
1620
2042
  onClick: handleClick,
1621
2043
  onDoubleClick: handleDoubleClick,
1622
2044
  onContextMenu: handleContextMenu,
@@ -1658,6 +2080,7 @@ function areRowPropsEqual(prev, next) {
1658
2080
  if (prev.activeColumnId !== next.activeColumnId) return false;
1659
2081
  if (prev.focusedColumnIndex !== next.focusedColumnIndex) return false;
1660
2082
  if (prev.hasFocusedCell !== next.hasFocusedCell) return false;
2083
+ if (prev.cellSelectionKey !== next.cellSelectionKey) return false;
1661
2084
  if (prev.virtualStyle !== next.virtualStyle) {
1662
2085
  if (!prev.virtualStyle || !next.virtualStyle) return false;
1663
2086
  if (prev.virtualStyle.transform !== next.virtualStyle.transform) return false;
@@ -1667,9 +2090,7 @@ function areRowPropsEqual(prev, next) {
1667
2090
  return true;
1668
2091
  }
1669
2092
  var MemoizedTableRow = React3.memo(TableRowInner, areRowPropsEqual);
1670
- function TableFooter({
1671
- table
1672
- }) {
2093
+ function TableFooter({ table }) {
1673
2094
  const footerGroups = table.getFooterGroups();
1674
2095
  if (!footerGroups.length) return null;
1675
2096
  return /* @__PURE__ */ jsx("tfoot", { className: "yable-tfoot", children: footerGroups.map((footerGroup) => /* @__PURE__ */ jsx("tr", { className: "yable-tr", children: footerGroup.headers.map((header) => {
@@ -1715,19 +2136,20 @@ function Spinner() {
1715
2136
  function LoadingOverlay({
1716
2137
  loading,
1717
2138
  loadingComponent,
1718
- loadingText = "Loading..."
2139
+ loadingText
1719
2140
  }) {
1720
2141
  if (!loading) return null;
2142
+ const resolvedText = loadingText ?? getDefaultLocale().loadingText;
1721
2143
  return /* @__PURE__ */ jsx(
1722
2144
  "div",
1723
2145
  {
1724
2146
  className: "yable-overlay-loading",
1725
2147
  role: "alert",
1726
2148
  "aria-busy": "true",
1727
- "aria-label": loadingText,
2149
+ "aria-label": resolvedText,
1728
2150
  children: /* @__PURE__ */ jsx("div", { className: "yable-overlay-loading-content", children: loadingComponent ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1729
2151
  /* @__PURE__ */ jsx(Spinner, {}),
1730
- loadingText && /* @__PURE__ */ jsx("span", { className: "yable-overlay-loading-text", children: loadingText })
2152
+ resolvedText && /* @__PURE__ */ jsx("span", { className: "yable-overlay-loading-text", children: resolvedText })
1731
2153
  ] }) })
1732
2154
  }
1733
2155
  );
@@ -1845,8 +2267,9 @@ function NoRowsOverlay({
1845
2267
  if (emptyComponent) {
1846
2268
  return /* @__PURE__ */ jsx("div", { className: "yable-overlay-empty", children: emptyComponent });
1847
2269
  }
1848
- const defaultMessage = isFiltered ? "No results found" : "No data";
1849
- const defaultDetail = isFiltered ? "Try adjusting your search or filter criteria." : "There are no rows to display.";
2270
+ const locale = getDefaultLocale();
2271
+ const defaultMessage = isFiltered ? locale.emptyNoResults : locale.emptyNoData;
2272
+ const defaultDetail = isFiltered ? locale.emptyNoResultsDetail : locale.emptyNoDataDetail;
1850
2273
  const icon = emptyIcon ?? (isFiltered ? /* @__PURE__ */ jsx(DefaultFilteredIcon, {}) : /* @__PURE__ */ jsx(DefaultEmptyIcon, {}));
1851
2274
  return /* @__PURE__ */ jsxs("div", { className: "yable-overlay-empty", role: "status", children: [
1852
2275
  /* @__PURE__ */ jsx("div", { className: "yable-overlay-empty-icon-wrapper", children: icon }),
@@ -2430,11 +2853,11 @@ function ContextMenu({
2430
2853
  }
2431
2854
  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2432
2855
  e.preventDefault();
2433
- const items2 = menuRef.current?.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])');
2434
- if (!items2 || items2.length === 0) return;
2435
- const currentIndex = Array.from(items2).findIndex(
2436
- (el) => el === document.activeElement
2856
+ const items2 = menuRef.current?.querySelectorAll(
2857
+ '[role="menuitem"]:not([aria-disabled="true"])'
2437
2858
  );
2859
+ if (!items2 || items2.length === 0) return;
2860
+ const currentIndex = Array.from(items2).findIndex((el) => el === document.activeElement);
2438
2861
  let nextIndex;
2439
2862
  if (e.key === "ArrowDown") {
2440
2863
  nextIndex = currentIndex < items2.length - 1 ? currentIndex + 1 : 0;
@@ -2479,11 +2902,7 @@ function ContextMenu({
2479
2902
  navigator.clipboard?.readText().then((text) => {
2480
2903
  const editing = table.getState().editing;
2481
2904
  if (editing?.activeCell) {
2482
- table.pasteFromClipboard(
2483
- text,
2484
- editing.activeCell.rowId,
2485
- editing.activeCell.columnId
2486
- );
2905
+ table.pasteFromClipboard(text, editing.activeCell.rowId, editing.activeCell.columnId);
2487
2906
  }
2488
2907
  }).catch(() => {
2489
2908
  });
@@ -2823,6 +3242,12 @@ function isEditableTarget(element) {
2823
3242
  }
2824
3243
  return element.isContentEditable;
2825
3244
  }
3245
+ function filterHeaderGroups(groups, visibleColumnIds) {
3246
+ return groups.map((group) => ({
3247
+ ...group,
3248
+ headers: group.headers.filter((header) => visibleColumnIds.has(header.column.id))
3249
+ })).filter((group) => group.headers.length > 0);
3250
+ }
2826
3251
  function Table({
2827
3252
  table,
2828
3253
  stickyHeader,
@@ -2835,7 +3260,7 @@ function Table({
2835
3260
  loading,
2836
3261
  loadingComponent,
2837
3262
  loadingText,
2838
- emptyMessage = "No data",
3263
+ emptyMessage,
2839
3264
  emptyComponent,
2840
3265
  emptyIcon,
2841
3266
  emptyDetail,
@@ -2849,6 +3274,10 @@ function Table({
2849
3274
  sidebar,
2850
3275
  sidebarPanels = ["columns", "filters"],
2851
3276
  defaultSidebarPanel,
3277
+ floatingFilters,
3278
+ columnVirtualization,
3279
+ columnVirtualizationOverscan,
3280
+ ariaLabel,
2852
3281
  ...rest
2853
3282
  }) {
2854
3283
  const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -2856,6 +3285,7 @@ function Table({
2856
3285
  defaultSidebarPanel ?? "columns"
2857
3286
  );
2858
3287
  const containerRef = useRef(null);
3288
+ const horizontalScrollRef = useRef(null);
2859
3289
  const isRtl = direction === "rtl";
2860
3290
  const classNames = [
2861
3291
  "yable",
@@ -2873,8 +3303,86 @@ function Table({
2873
3303
  const hasGlobalFilter = Boolean(table.getState().globalFilter);
2874
3304
  const hasColumnFilters = table.getState().columnFilters.length > 0;
2875
3305
  const isFiltered = hasGlobalFilter || hasColumnFilters;
3306
+ const allVisibleColumns = table.getVisibleLeafColumns();
3307
+ const hasPinnedColumns = table.getLeftVisibleLeafColumns().length > 0 || table.getRightVisibleLeafColumns().length > 0;
3308
+ const hasGroupedHeaders = table.getHeaderGroups().length > 1;
3309
+ const canVirtualizeColumns = Boolean(columnVirtualization) && !hasPinnedColumns && !hasGroupedHeaders && allVisibleColumns.length > 0;
3310
+ const columnVirtualState = useColumnVirtualization({
3311
+ containerRef: horizontalScrollRef,
3312
+ columns: allVisibleColumns,
3313
+ overscan: columnVirtualizationOverscan ?? 2,
3314
+ enabled: canVirtualizeColumns
3315
+ });
3316
+ const renderTable = useMemo(() => {
3317
+ if (!canVirtualizeColumns || !columnVirtualState.isVirtualized) {
3318
+ return table;
3319
+ }
3320
+ const virtualColumns = columnVirtualState.virtualColumns.map((entry) => entry.column);
3321
+ const visibleColumnIds = new Set(virtualColumns.map((column) => column.id));
3322
+ const next = Object.create(table);
3323
+ next.getVisibleFlatColumns = () => virtualColumns;
3324
+ next.getVisibleLeafColumns = () => virtualColumns;
3325
+ next.getCenterVisibleLeafColumns = () => virtualColumns;
3326
+ next.getLeftVisibleLeafColumns = () => [];
3327
+ next.getRightVisibleLeafColumns = () => [];
3328
+ next.getHeaderGroups = () => filterHeaderGroups(table.getHeaderGroups(), visibleColumnIds);
3329
+ next.getCenterHeaderGroups = () => filterHeaderGroups(table.getCenterHeaderGroups(), visibleColumnIds);
3330
+ next.getFooterGroups = () => filterHeaderGroups(table.getFooterGroups(), visibleColumnIds);
3331
+ next.getCenterFooterGroups = () => filterHeaderGroups(table.getCenterFooterGroups(), visibleColumnIds);
3332
+ return next;
3333
+ }, [
3334
+ canVirtualizeColumns,
3335
+ columnVirtualState.isVirtualized,
3336
+ columnVirtualState.virtualColumns,
3337
+ table
3338
+ ]);
3339
+ const showColumnVirtualizationShell = canVirtualizeColumns;
2876
3340
  const contextMenu = useContextMenu();
2877
3341
  useKeyboardNavigation(table, { containerRef });
3342
+ const [announcement, setAnnouncement] = useState("");
3343
+ const prevSortingRef = useRef(table.getState().sorting);
3344
+ const prevFilterCountRef = useRef(rows.length);
3345
+ const prevHasFiltersRef = useRef(isFiltered);
3346
+ const prevPaginationRef = useRef(table.getState().pagination);
3347
+ const isFirstRenderRef = useRef(true);
3348
+ useEffect(() => {
3349
+ if (isFirstRenderRef.current) {
3350
+ isFirstRenderRef.current = false;
3351
+ return;
3352
+ }
3353
+ const currentSorting = table.getState().sorting;
3354
+ const currentPagination = table.getState().pagination;
3355
+ const currentRowCount = rows.length;
3356
+ const currentIsFiltered = isFiltered;
3357
+ if (JSON.stringify(currentSorting) !== JSON.stringify(prevSortingRef.current)) {
3358
+ prevSortingRef.current = currentSorting;
3359
+ const firstSort = currentSorting[0];
3360
+ if (firstSort) {
3361
+ const column = table.getColumn(firstSort.id);
3362
+ const headerDef = column?.columnDef.header;
3363
+ const columnName = typeof headerDef === "string" ? headerDef : firstSort.id;
3364
+ const direction2 = firstSort.desc ? "descending" : "ascending";
3365
+ setAnnouncement(`Sorted by ${columnName} ${direction2}`);
3366
+ return;
3367
+ }
3368
+ }
3369
+ if (currentRowCount !== prevFilterCountRef.current || currentIsFiltered !== prevHasFiltersRef.current) {
3370
+ prevFilterCountRef.current = currentRowCount;
3371
+ prevHasFiltersRef.current = currentIsFiltered;
3372
+ if (currentIsFiltered) {
3373
+ setAnnouncement(`${currentRowCount} rows after filtering`);
3374
+ return;
3375
+ }
3376
+ }
3377
+ if (JSON.stringify(currentPagination) !== JSON.stringify(prevPaginationRef.current)) {
3378
+ prevPaginationRef.current = currentPagination;
3379
+ const pageCount = table.getPageCount();
3380
+ if (pageCount > 0) {
3381
+ setAnnouncement(`Page ${currentPagination.pageIndex + 1} of ${pageCount}`);
3382
+ return;
3383
+ }
3384
+ }
3385
+ });
2878
3386
  const handleContextMenu = useCallback(
2879
3387
  (e) => {
2880
3388
  e.preventDefault();
@@ -2882,6 +3390,24 @@ function Table({
2882
3390
  },
2883
3391
  [contextMenu, table]
2884
3392
  );
3393
+ const tableNode = /* @__PURE__ */ jsxs(
3394
+ "table",
3395
+ {
3396
+ className: "yable-table",
3397
+ style: columnVirtualState.isVirtualized ? {
3398
+ width: columnVirtualState.visibleWidth,
3399
+ minWidth: columnVirtualState.visibleWidth,
3400
+ marginLeft: columnVirtualState.startOffset,
3401
+ tableLayout: "fixed"
3402
+ } : void 0,
3403
+ "data-column-virtualized": columnVirtualState.isVirtualized || void 0,
3404
+ children: [
3405
+ /* @__PURE__ */ jsx(TableHeader, { table: renderTable, floatingFilters }),
3406
+ /* @__PURE__ */ jsx(TableBody, { table: renderTable, clickableRows }),
3407
+ footer && /* @__PURE__ */ jsx(TableFooter, { table: renderTable })
3408
+ ]
3409
+ }
3410
+ );
2885
3411
  return /* @__PURE__ */ jsx(TableProvider, { value: table, children: /* @__PURE__ */ jsxs(
2886
3412
  "div",
2887
3413
  {
@@ -2890,23 +3416,40 @@ function Table({
2890
3416
  "data-theme": theme,
2891
3417
  dir: direction,
2892
3418
  role: "grid",
3419
+ "aria-label": ariaLabel ?? "Data table",
2893
3420
  "aria-rowcount": table.getRowModel().rows.length,
2894
3421
  "aria-colcount": table.getVisibleLeafColumns().length,
2895
3422
  onContextMenu: handleContextMenu,
2896
3423
  ...rest,
2897
3424
  children: [
2898
3425
  /* @__PURE__ */ jsxs("div", { className: "yable-main", children: [
2899
- /* @__PURE__ */ jsxs("table", { className: "yable-table", children: [
2900
- /* @__PURE__ */ jsx(TableHeader, { table }),
2901
- /* @__PURE__ */ jsx(
2902
- TableBody,
2903
- {
2904
- table,
2905
- clickableRows
2906
- }
2907
- ),
2908
- footer && /* @__PURE__ */ jsx(TableFooter, { table })
2909
- ] }),
3426
+ showColumnVirtualizationShell ? /* @__PURE__ */ jsx(
3427
+ "div",
3428
+ {
3429
+ ref: horizontalScrollRef,
3430
+ className: "yable-horizontal-scroll-container",
3431
+ style: {
3432
+ overflowX: "auto",
3433
+ overflowY: "visible",
3434
+ maxWidth: "100%",
3435
+ position: "relative"
3436
+ },
3437
+ children: /* @__PURE__ */ jsx(
3438
+ "div",
3439
+ {
3440
+ className: "yable-horizontal-scroll-inner",
3441
+ style: {
3442
+ width: Math.max(columnVirtualState.totalWidth, columnVirtualState.visibleWidth),
3443
+ minWidth: Math.max(
3444
+ columnVirtualState.totalWidth,
3445
+ columnVirtualState.visibleWidth
3446
+ )
3447
+ },
3448
+ children: tableNode
3449
+ }
3450
+ )
3451
+ }
3452
+ ) : tableNode,
2910
3453
  /* @__PURE__ */ jsx(
2911
3454
  LoadingOverlay,
2912
3455
  {
@@ -2947,6 +3490,24 @@ function Table({
2947
3490
  onClose: contextMenu.close,
2948
3491
  table
2949
3492
  }
3493
+ ),
3494
+ /* @__PURE__ */ jsx(
3495
+ "div",
3496
+ {
3497
+ role: "status",
3498
+ "aria-live": "polite",
3499
+ "aria-atomic": "true",
3500
+ style: {
3501
+ position: "absolute",
3502
+ width: "1px",
3503
+ height: "1px",
3504
+ overflow: "hidden",
3505
+ clip: "rect(0,0,0,0)",
3506
+ whiteSpace: "nowrap",
3507
+ border: 0
3508
+ },
3509
+ children: announcement
3510
+ }
2950
3511
  )
2951
3512
  ]
2952
3513
  }
@@ -2987,9 +3548,10 @@ function Pagination({
2987
3548
  const to = Math.min((pageIndex + 1) * pageSize, totalRows);
2988
3549
  const canPrev = table.getCanPreviousPage();
2989
3550
  const canNext = table.getCanNextPage();
3551
+ const locale = getDefaultLocale();
2990
3552
  return /* @__PURE__ */ jsxs("nav", { className: "yable-pagination", role: "navigation", "aria-label": "Table pagination", children: [
2991
3553
  showInfo && /* @__PURE__ */ jsxs("div", { className: "yable-pagination-info", children: [
2992
- /* @__PURE__ */ jsx("span", { className: "yable-pagination-info-text", children: totalRows > 0 ? `${from}\u2013${to} of ${totalRows}` : "No results" }),
3554
+ /* @__PURE__ */ jsx("span", { className: "yable-pagination-info-text", children: totalRows > 0 ? `${from}\u2013${to} ${locale.paginationOf} ${totalRows}` : locale.paginationNoResults }),
2993
3555
  showPageSize && /* @__PURE__ */ jsxs("div", { className: "yable-pagination-select-wrapper", children: [
2994
3556
  /* @__PURE__ */ jsx(
2995
3557
  "select",
@@ -3002,7 +3564,8 @@ function Pagination({
3002
3564
  "aria-label": "Rows per page",
3003
3565
  children: pageSizes.map((size) => /* @__PURE__ */ jsxs("option", { value: size, children: [
3004
3566
  size,
3005
- " rows"
3567
+ " ",
3568
+ locale.paginationRows
3006
3569
  ] }, size))
3007
3570
  }
3008
3571
  ),
@@ -3017,8 +3580,8 @@ function Pagination({
3017
3580
  className: "yable-pagination-btn yable-pagination-btn--nav",
3018
3581
  onClick: () => table.setPageIndex(0),
3019
3582
  disabled: !canPrev,
3020
- "aria-label": "First page",
3021
- title: "First page",
3583
+ "aria-label": locale.paginationFirstPage,
3584
+ title: locale.paginationFirstPage,
3022
3585
  children: /* @__PURE__ */ jsx(ChevronFirstIcon, {})
3023
3586
  }
3024
3587
  ),
@@ -3029,8 +3592,8 @@ function Pagination({
3029
3592
  className: "yable-pagination-btn yable-pagination-btn--nav",
3030
3593
  onClick: () => table.previousPage(),
3031
3594
  disabled: !canPrev,
3032
- "aria-label": "Previous page",
3033
- title: "Previous page",
3595
+ "aria-label": locale.paginationPreviousPage,
3596
+ title: locale.paginationPreviousPage,
3034
3597
  children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
3035
3598
  }
3036
3599
  ),
@@ -3054,7 +3617,7 @@ function Pagination({
3054
3617
  className: `yable-pagination-btn yable-pagination-btn--page${page === pageIndex ? " yable-pagination-btn--active" : ""}`,
3055
3618
  "data-active": page === pageIndex ? "true" : void 0,
3056
3619
  onClick: () => table.setPageIndex(page),
3057
- "aria-label": `Page ${page + 1}`,
3620
+ "aria-label": `${locale.paginationPage} ${page + 1}`,
3058
3621
  "aria-current": page === pageIndex ? "page" : void 0,
3059
3622
  children: page + 1
3060
3623
  },
@@ -3068,8 +3631,8 @@ function Pagination({
3068
3631
  className: "yable-pagination-btn yable-pagination-btn--nav",
3069
3632
  onClick: () => table.nextPage(),
3070
3633
  disabled: !canNext,
3071
- "aria-label": "Next page",
3072
- title: "Next page",
3634
+ "aria-label": locale.paginationNextPage,
3635
+ title: locale.paginationNextPage,
3073
3636
  children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
3074
3637
  }
3075
3638
  ),
@@ -3080,8 +3643,8 @@ function Pagination({
3080
3643
  className: "yable-pagination-btn yable-pagination-btn--nav",
3081
3644
  onClick: () => table.setPageIndex(pageCount - 1),
3082
3645
  disabled: !canNext,
3083
- "aria-label": "Last page",
3084
- title: "Last page",
3646
+ "aria-label": locale.paginationLastPage,
3647
+ title: locale.paginationLastPage,
3085
3648
  children: /* @__PURE__ */ jsx(ChevronLastIcon, {})
3086
3649
  }
3087
3650
  )
@@ -3130,10 +3693,12 @@ function ClearIcon() {
3130
3693
  }
3131
3694
  function GlobalFilter({
3132
3695
  table,
3133
- placeholder = "Search...",
3696
+ placeholder,
3134
3697
  debounce = 300,
3135
3698
  className
3136
3699
  }) {
3700
+ const locale = getDefaultLocale();
3701
+ const resolvedPlaceholder = placeholder ?? locale.searchPlaceholder;
3137
3702
  const [value, setValue] = useState(
3138
3703
  table.getState().globalFilter ?? ""
3139
3704
  );
@@ -3190,8 +3755,8 @@ function GlobalFilter({
3190
3755
  value,
3191
3756
  onChange: handleChange,
3192
3757
  onKeyDown: handleKeyDown,
3193
- placeholder,
3194
- "aria-label": "Search table",
3758
+ placeholder: resolvedPlaceholder,
3759
+ "aria-label": locale.searchAriaLabel,
3195
3760
  role: "searchbox"
3196
3761
  }
3197
3762
  ),
@@ -3278,10 +3843,25 @@ function CellSelect({
3278
3843
  const isAlwaysEditable = cell.getIsAlwaysEditable();
3279
3844
  const pending = table.getPendingValue(row.id, column.id);
3280
3845
  const currentValue = pending !== void 0 ? pending : cell.getValue();
3846
+ const commitTimerRef = useRef(null);
3847
+ useEffect(() => {
3848
+ return () => {
3849
+ if (commitTimerRef.current !== null) {
3850
+ clearTimeout(commitTimerRef.current);
3851
+ commitTimerRef.current = null;
3852
+ }
3853
+ };
3854
+ }, []);
3281
3855
  const handleChange = (e) => {
3282
3856
  table.setPendingValue(row.id, column.id, e.target.value);
3283
3857
  if (isEditing && !isAlwaysEditable) {
3284
- setTimeout(() => table.commitEdit(), 0);
3858
+ if (commitTimerRef.current !== null) {
3859
+ clearTimeout(commitTimerRef.current);
3860
+ }
3861
+ commitTimerRef.current = setTimeout(() => {
3862
+ commitTimerRef.current = null;
3863
+ table.commitEdit();
3864
+ }, 0);
3285
3865
  }
3286
3866
  };
3287
3867
  return /* @__PURE__ */ jsxs(
@@ -3393,14 +3973,7 @@ function formatDateValue(value, type) {
3393
3973
  return String(value);
3394
3974
  }
3395
3975
  function useClipboard(table, options = {}) {
3396
- const {
3397
- enabled = true,
3398
- containerRef,
3399
- onCopy,
3400
- onCut,
3401
- onPaste,
3402
- ...clipboardOptions
3403
- } = options;
3976
+ const { enabled = true, containerRef, onCopy, onCut, onPaste, ...clipboardOptions } = options;
3404
3977
  const handleCopy = useCallback(
3405
3978
  (e) => {
3406
3979
  if (isEditableTarget2(e.target)) return;
@@ -3438,20 +4011,29 @@ function useClipboard(table, options = {}) {
3438
4011
  targetRowId = state.editing.activeCell.rowId;
3439
4012
  targetColumnId = state.editing.activeCell.columnId;
3440
4013
  } else {
3441
- const selectedRowIds = Object.keys(state.rowSelection || {}).filter(
3442
- (id) => state.rowSelection[id]
3443
- );
3444
- if (selectedRowIds.length > 0) {
3445
- targetRowId = selectedRowIds[0];
4014
+ const selectedRange = table.getCellSelectionRange();
4015
+ const rows = table.getRowModel().rows;
4016
+ const columns = table.getVisibleLeafColumns();
4017
+ if (selectedRange) {
4018
+ const startRowIndex = Math.min(selectedRange.start.rowIndex, selectedRange.end.rowIndex);
4019
+ const startColumnIndex = Math.min(
4020
+ selectedRange.start.columnIndex,
4021
+ selectedRange.end.columnIndex
4022
+ );
4023
+ targetRowId = rows[startRowIndex]?.id;
4024
+ targetColumnId = columns[startColumnIndex]?.id;
3446
4025
  } else {
3447
- const rows = table.getRowModel().rows;
3448
- if (rows.length > 0) {
4026
+ const selectedRowIds = Object.keys(state.rowSelection || {}).filter(
4027
+ (id) => state.rowSelection[id]
4028
+ );
4029
+ if (selectedRowIds.length > 0) {
4030
+ targetRowId = selectedRowIds[0];
4031
+ } else if (rows.length > 0) {
3449
4032
  targetRowId = rows[0].id;
3450
4033
  }
3451
- }
3452
- const columns = table.getVisibleLeafColumns();
3453
- if (columns.length > 0) {
3454
- targetColumnId = columns[0].id;
4034
+ if (columns.length > 0) {
4035
+ targetColumnId = columns[0].id;
4036
+ }
3455
4037
  }
3456
4038
  }
3457
4039
  if (targetRowId && targetColumnId) {
@@ -4741,7 +5323,7 @@ function sanitizeCSS(css) {
4741
5323
  sanitized = sanitized.replace(/javascript\s*:/gi, "/* javascript: removed */");
4742
5324
  return sanitized;
4743
5325
  }
4744
- function usePrintLayout(table, options = {}) {
5326
+ function usePrintLayout(_table, options = {}) {
4745
5327
  const { title, additionalCSS } = options;
4746
5328
  const isPrintingRef = useRef(false);
4747
5329
  const preparePrint = useCallback(() => {
@@ -4778,7 +5360,7 @@ function usePrintLayout(table, options = {}) {
4778
5360
  requestAnimationFrame(() => {
4779
5361
  window.print();
4780
5362
  });
4781
- }, [table, title, additionalCSS]);
5363
+ }, [title, additionalCSS]);
4782
5364
  return {
4783
5365
  preparePrint,
4784
5366
  isPrinting: isPrintingRef.current
@@ -4830,6 +5412,6 @@ function useTheme(options = {}) {
4830
5412
  };
4831
5413
  }
4832
5414
 
4833
- export { CellBadge, CellBoolean, CellCheckbox, CellCurrency, CellDate, CellDatePicker, CellErrorBoundary, CellInput, CellLink, CellNumeric, CellProgress, CellRating, CellSelect, CellStatus, CellStatusBadge, CellToggle, ColumnsPanel, ContextMenu, ContextMenuItem, DEFAULT_TEXT_RECIPE, DragHandle, ErrorBoundary, ExpandIcon, FillHandle, FiltersPanel, FlashCell, GlobalFilter, LoadingOverlay, MasterDetail, NoRowsOverlay, Pagination, PivotConfigPanel, PrintLayout, Sidebar, SortIndicator, StatusBar, StatusBarPanelComponent, Table, TableBody, TableCell, TableFooter, TableHeader, TableProvider, Tooltip, TreeToggle, getMeasureRecipeForCellType, getRegisteredCellTypes, resolveMeasureRecipe, useAutoMeasurements, useCellFlash, useClipboard, useContextMenu, useFillHandle, useKeyboardNavigation, usePretextMeasurement, usePrintLayout, useRowAnimation, useRowDrag, useTable, useTableContext, useTableRowHeights, useTheme, useTooltip, useVirtualization };
5415
+ export { CellBadge, CellBoolean, CellCheckbox, CellCurrency, CellDate, CellDatePicker, CellErrorBoundary, CellInput, CellLink, CellNumeric, CellProgress, CellRating, CellSelect, CellStatus, CellStatusBadge, CellToggle, ColumnsPanel, ContextMenu, ContextMenuItem, DEFAULT_TEXT_RECIPE, DragHandle, ErrorBoundary, ExpandIcon, FillHandle, FiltersPanel, FlashCell, FloatingFilter, GlobalFilter, LoadingOverlay, MasterDetail, NoRowsOverlay, Pagination, PivotConfigPanel, PrintLayout, SetFilter, Sidebar, SortIndicator, StatusBar, StatusBarPanelComponent, Table, TableBody, TableCell, TableFooter, TableHeader, TableProvider, Tooltip, TreeToggle, getMeasureRecipeForCellType, getRegisteredCellTypes, resolveMeasureRecipe, useAutoMeasurements, useCellFlash, useClipboard, useColumnVirtualization, useContextMenu, useFillHandle, useKeyboardNavigation, usePretextMeasurement, usePrintLayout, useRowAnimation, useRowDrag, useTable, useTableContext, useTableRowHeights, useTheme, useTooltip, useVirtualization };
4834
5416
  //# sourceMappingURL=index.js.map
4835
5417
  //# sourceMappingURL=index.js.map