@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.cjs CHANGED
@@ -69,6 +69,8 @@ function useTable(options) {
69
69
  prevOptionsRef.current = options;
70
70
  return options;
71
71
  }, [options]);
72
+ const onStateChangeRef = React3.useRef(options.onStateChange);
73
+ onStateChangeRef.current = options.onStateChange;
72
74
  const resolvedState = React3.useMemo(
73
75
  () => ({
74
76
  ...state,
@@ -78,13 +80,14 @@ function useTable(options) {
78
80
  );
79
81
  const onStateChange = React3.useCallback(
80
82
  (updater) => {
81
- if (stableOptions.onStateChange) {
82
- stableOptions.onStateChange(updater);
83
+ const latest = onStateChangeRef.current;
84
+ if (latest) {
85
+ latest(updater);
83
86
  } else {
84
87
  setState((prev) => yableCore.functionalUpdate(updater, prev));
85
88
  }
86
89
  },
87
- [stableOptions.onStateChange]
90
+ []
88
91
  );
89
92
  const resolvedOptions = React3.useMemo(
90
93
  () => ({
@@ -212,7 +215,15 @@ function useVirtualization({
212
215
  container.scrollTop = offset;
213
216
  }
214
217
  },
215
- [containerRef, totalRows, rowHeight, isFixedHeight, getRowHeight, hasPretextHeights, pretextPrefixSums]
218
+ [
219
+ containerRef,
220
+ totalRows,
221
+ rowHeight,
222
+ isFixedHeight,
223
+ getRowHeight,
224
+ hasPretextHeights,
225
+ pretextPrefixSums
226
+ ]
216
227
  );
217
228
  const totalHeight = React3.useMemo(() => {
218
229
  if (totalRows === 0) return 0;
@@ -266,10 +277,7 @@ function useVirtualization({
266
277
  } else if (isFixedHeight) {
267
278
  const fixedH = rowHeight;
268
279
  startIndex = Math.floor(scrollTop / fixedH);
269
- endIndex = Math.min(
270
- totalRows - 1,
271
- Math.ceil((scrollTop + containerHeight) / fixedH) - 1
272
- );
280
+ endIndex = Math.min(totalRows - 1, Math.ceil((scrollTop + containerHeight) / fixedH) - 1);
273
281
  } else {
274
282
  let accum = 0;
275
283
  let foundStart = false;
@@ -336,6 +344,162 @@ function useVirtualization({
336
344
  scrollTo
337
345
  };
338
346
  }
347
+ function binarySearchOffsets(offsets, target) {
348
+ let low = 0;
349
+ let high = offsets.length - 1;
350
+ while (low < high) {
351
+ const mid = low + high >>> 1;
352
+ if (offsets[mid + 1] <= target) {
353
+ low = mid + 1;
354
+ } else {
355
+ high = mid;
356
+ }
357
+ }
358
+ return low;
359
+ }
360
+ function useColumnVirtualization({
361
+ containerRef,
362
+ columns,
363
+ overscan = 2,
364
+ enabled = true
365
+ }) {
366
+ const [scrollState, setScrollState] = React3.useState({
367
+ scrollLeft: 0,
368
+ containerWidth: 0
369
+ });
370
+ const rafRef = React3.useRef(null);
371
+ const sizes = React3.useMemo(() => columns.map((column) => column.getSize()), [columns]);
372
+ const offsets = React3.useMemo(() => {
373
+ const next = new Array(columns.length + 1);
374
+ next[0] = 0;
375
+ for (let i = 0; i < columns.length; i++) {
376
+ next[i + 1] = next[i] + (sizes[i] ?? 0);
377
+ }
378
+ return next;
379
+ }, [columns.length, sizes]);
380
+ const totalWidth = offsets[offsets.length - 1] ?? 0;
381
+ React3.useEffect(() => {
382
+ const container = containerRef.current;
383
+ if (!container) return;
384
+ setScrollState({
385
+ scrollLeft: container.scrollLeft,
386
+ containerWidth: container.clientWidth
387
+ });
388
+ const handleScroll = () => {
389
+ if (rafRef.current !== null) return;
390
+ rafRef.current = requestAnimationFrame(() => {
391
+ rafRef.current = null;
392
+ const el = containerRef.current;
393
+ if (!el) return;
394
+ setScrollState({
395
+ scrollLeft: el.scrollLeft,
396
+ containerWidth: el.clientWidth
397
+ });
398
+ });
399
+ };
400
+ container.addEventListener("scroll", handleScroll, { passive: true });
401
+ let resizeObserver;
402
+ if (typeof ResizeObserver !== "undefined") {
403
+ resizeObserver = new ResizeObserver(() => {
404
+ const el = containerRef.current;
405
+ if (!el) return;
406
+ setScrollState((prev) => {
407
+ const nextWidth = el.clientWidth;
408
+ if (prev.containerWidth === nextWidth && prev.scrollLeft === el.scrollLeft) {
409
+ return prev;
410
+ }
411
+ return {
412
+ scrollLeft: el.scrollLeft,
413
+ containerWidth: nextWidth
414
+ };
415
+ });
416
+ });
417
+ resizeObserver.observe(container);
418
+ }
419
+ return () => {
420
+ container.removeEventListener("scroll", handleScroll);
421
+ if (rafRef.current !== null) {
422
+ cancelAnimationFrame(rafRef.current);
423
+ rafRef.current = null;
424
+ }
425
+ resizeObserver?.disconnect();
426
+ };
427
+ }, [containerRef]);
428
+ const scrollToIndex = React3.useCallback(
429
+ (index) => {
430
+ const container = containerRef.current;
431
+ if (!container || columns.length === 0) return;
432
+ const clampedIndex = Math.max(0, Math.min(index, columns.length - 1));
433
+ container.scrollLeft = offsets[clampedIndex] ?? 0;
434
+ },
435
+ [columns.length, containerRef, offsets]
436
+ );
437
+ if (!enabled || columns.length === 0) {
438
+ return {
439
+ virtualColumns: columns.map((column, index) => ({
440
+ column,
441
+ index,
442
+ start: offsets[index] ?? 0,
443
+ size: sizes[index] ?? 0
444
+ })),
445
+ startOffset: 0,
446
+ endOffset: 0,
447
+ totalWidth,
448
+ visibleWidth: totalWidth,
449
+ startIndex: 0,
450
+ endIndex: Math.max(columns.length - 1, 0),
451
+ isVirtualized: false,
452
+ scrollToIndex
453
+ };
454
+ }
455
+ const { scrollLeft, containerWidth } = scrollState;
456
+ if (containerWidth <= 0 || totalWidth <= containerWidth) {
457
+ return {
458
+ virtualColumns: columns.map((column, index) => ({
459
+ column,
460
+ index,
461
+ start: offsets[index] ?? 0,
462
+ size: sizes[index] ?? 0
463
+ })),
464
+ startOffset: 0,
465
+ endOffset: 0,
466
+ totalWidth,
467
+ visibleWidth: totalWidth,
468
+ startIndex: 0,
469
+ endIndex: Math.max(columns.length - 1, 0),
470
+ isVirtualized: false,
471
+ scrollToIndex
472
+ };
473
+ }
474
+ const startIndex = binarySearchOffsets(offsets, scrollLeft);
475
+ const endBoundary = scrollLeft + containerWidth;
476
+ const endIndex = binarySearchOffsets(offsets, Math.max(scrollLeft, endBoundary - 1));
477
+ const overscanStart = Math.max(0, startIndex - overscan);
478
+ const overscanEnd = Math.min(columns.length - 1, endIndex + overscan);
479
+ const virtualColumns = [];
480
+ for (let i = overscanStart; i <= overscanEnd; i++) {
481
+ virtualColumns.push({
482
+ column: columns[i],
483
+ index: i,
484
+ start: offsets[i] ?? 0,
485
+ size: sizes[i] ?? 0
486
+ });
487
+ }
488
+ const startOffset = offsets[overscanStart] ?? 0;
489
+ const visibleWidth = (offsets[overscanEnd + 1] ?? totalWidth) - startOffset;
490
+ const endOffset = totalWidth - (offsets[overscanEnd + 1] ?? totalWidth);
491
+ return {
492
+ virtualColumns,
493
+ startOffset,
494
+ endOffset,
495
+ totalWidth,
496
+ visibleWidth,
497
+ startIndex: overscanStart,
498
+ endIndex: overscanEnd,
499
+ isVirtualized: true,
500
+ scrollToIndex
501
+ };
502
+ }
339
503
  var pretextPromise = null;
340
504
  function loadPretext() {
341
505
  if (pretextPromise) return pretextPromise;
@@ -386,7 +550,12 @@ function usePretextMeasurement({
386
550
  }
387
551
  prepareTimeRef.current = performance.now() - start;
388
552
  return result;
389
- }, [pretext, enabled, data, columns.map((c) => `${c.columnId}:${c.font}:${c.fixedHeight ? "F" : "T"}`).join("|")]);
553
+ }, [
554
+ pretext,
555
+ enabled,
556
+ data,
557
+ columns.map((c) => `${c.columnId}:${c.font}:${c.fixedHeight ? "F" : "T"}`).join("|")
558
+ ]);
390
559
  const measurement = React3.useMemo(() => {
391
560
  if (!pretext || !preparedCells) {
392
561
  return { rowHeights: null, prefixSums: null, totalHeight: 0 };
@@ -424,6 +593,7 @@ function usePretextMeasurement({
424
593
  return {
425
594
  rowHeights,
426
595
  prefixSums,
596
+ // Safe: prefixSums has length data.length + 1, so [data.length] always exists
427
597
  totalHeight: prefixSums[data.length]
428
598
  };
429
599
  }, [pretext, preparedCells, columnWidthsKey, minRowHeight, data.length]);
@@ -688,18 +858,10 @@ function CellDate({
688
858
  if (raw == null) return null;
689
859
  const date = raw instanceof Date ? raw : new Date(String(raw));
690
860
  if (isNaN(date.getTime())) return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "yable-cell-date", children: String(raw) });
691
- const formatter = React3.useMemo(() => {
692
- if (typeof format === "string" && format !== "relative") {
693
- return new Intl.DateTimeFormat(locale, {
694
- ...PRESETS[format],
695
- timeZone: "UTC"
696
- });
697
- }
698
- if (typeof format === "object") {
699
- return new Intl.DateTimeFormat(locale, { ...format, timeZone: "UTC" });
700
- }
701
- return null;
702
- }, [format, locale]);
861
+ const formatter = typeof format === "string" && format !== "relative" ? new Intl.DateTimeFormat(locale, {
862
+ ...PRESETS[format],
863
+ timeZone: "UTC"
864
+ }) : typeof format === "object" ? new Intl.DateTimeFormat(locale, { ...format, timeZone: "UTC" }) : null;
703
865
  const formatted = format === "relative" ? formatRelative(date) : formatter ? formatter.format(date) : date.toLocaleDateString(locale);
704
866
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `yable-cell-date ${className ?? ""}`, title: date.toISOString(), children: formatted });
705
867
  }
@@ -710,8 +872,14 @@ var measureRecipe9 = {
710
872
  padding: 20
711
873
  };
712
874
  function isSafeUrl(url) {
713
- const normalized = String(url).toLowerCase().trim();
714
- return !normalized.startsWith("javascript:") && !normalized.startsWith("data:text/html") && !normalized.startsWith("vbscript:");
875
+ const normalized = String(url).trim();
876
+ if (/^[/#?]/.test(normalized)) return true;
877
+ try {
878
+ const parsed = new URL(normalized, "https://placeholder.invalid");
879
+ return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
880
+ } catch {
881
+ return false;
882
+ }
715
883
  }
716
884
  function CellLink({
717
885
  context,
@@ -888,7 +1056,7 @@ function useTableContext() {
888
1056
  const ctx = React3.useContext(TableContext);
889
1057
  if (!ctx) {
890
1058
  throw new Error(
891
- "[yable] useTableContext must be used within a <Table> component. Did you forget to pass the `table` prop?"
1059
+ "[yable E001] useTableContext must be used within a <Table> component. Did you forget to pass the `table` prop?"
892
1060
  );
893
1061
  }
894
1062
  return ctx;
@@ -938,12 +1106,234 @@ function SortIndicator({ direction, index }) {
938
1106
  }
939
1107
  );
940
1108
  }
1109
+ function formatValue(value) {
1110
+ if (value == null || value === "") return "(empty)";
1111
+ if (typeof value === "string") return value;
1112
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1113
+ if (value instanceof Date) return value.toISOString();
1114
+ return JSON.stringify(value);
1115
+ }
1116
+ function SetFilter({ column, className }) {
1117
+ const [open, setOpen] = React3.useState(false);
1118
+ const filterValue = column.getFilterValue();
1119
+ const selectedValues = Array.isArray(filterValue) ? filterValue : filterValue == null || filterValue === "" ? [] : [filterValue];
1120
+ const facetedUniqueValues = column.getFacetedUniqueValues();
1121
+ const options = Array.from(facetedUniqueValues.entries()).map(([value, count]) => ({
1122
+ value,
1123
+ count,
1124
+ label: formatValue(value)
1125
+ })).sort((a, b) => a.label.localeCompare(b.label));
1126
+ const selectedSet = new Set(selectedValues);
1127
+ const toggleValue = (value) => {
1128
+ const next = new Set(selectedSet);
1129
+ if (next.has(value)) {
1130
+ next.delete(value);
1131
+ } else {
1132
+ next.add(value);
1133
+ }
1134
+ const nextValues = Array.from(next);
1135
+ column.setFilterValue(nextValues.length > 0 ? nextValues : void 0);
1136
+ };
1137
+ const headerLabel = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
1138
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1139
+ "div",
1140
+ {
1141
+ className: ["yable-set-filter", className].filter(Boolean).join(" "),
1142
+ style: { position: "relative" },
1143
+ children: [
1144
+ /* @__PURE__ */ jsxRuntime.jsx(
1145
+ "button",
1146
+ {
1147
+ type: "button",
1148
+ className: "yable-set-filter-trigger",
1149
+ "aria-haspopup": "dialog",
1150
+ "aria-expanded": open,
1151
+ onClick: () => setOpen((prev) => !prev),
1152
+ style: {
1153
+ width: "100%",
1154
+ minHeight: 28,
1155
+ padding: "4px 8px",
1156
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1157
+ borderRadius: 6,
1158
+ background: "transparent",
1159
+ font: "inherit",
1160
+ textAlign: "left",
1161
+ cursor: "pointer"
1162
+ },
1163
+ children: selectedValues.length > 0 ? `${selectedValues.length} selected` : `Filter ${headerLabel}`
1164
+ }
1165
+ ),
1166
+ open && /* @__PURE__ */ jsxRuntime.jsxs(
1167
+ "div",
1168
+ {
1169
+ role: "dialog",
1170
+ "aria-label": `${headerLabel} set filter`,
1171
+ className: "yable-set-filter-popover",
1172
+ style: {
1173
+ position: "absolute",
1174
+ insetInlineStart: 0,
1175
+ top: "calc(100% + 4px)",
1176
+ zIndex: 20,
1177
+ width: 220,
1178
+ maxHeight: 240,
1179
+ overflow: "auto",
1180
+ padding: 8,
1181
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1182
+ borderRadius: 8,
1183
+ background: "var(--yable-color-bg, #fff)",
1184
+ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.12)"
1185
+ },
1186
+ children: [
1187
+ /* @__PURE__ */ jsxRuntime.jsxs(
1188
+ "div",
1189
+ {
1190
+ style: {
1191
+ display: "flex",
1192
+ alignItems: "center",
1193
+ justifyContent: "space-between",
1194
+ gap: 8,
1195
+ marginBottom: 8
1196
+ },
1197
+ children: [
1198
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { style: { fontSize: 12 }, children: headerLabel }),
1199
+ selectedValues.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1200
+ "button",
1201
+ {
1202
+ type: "button",
1203
+ onClick: () => column.setFilterValue(void 0),
1204
+ style: {
1205
+ border: "none",
1206
+ background: "transparent",
1207
+ padding: 0,
1208
+ cursor: "pointer",
1209
+ fontSize: 12
1210
+ },
1211
+ children: "Clear"
1212
+ }
1213
+ )
1214
+ ]
1215
+ }
1216
+ ),
1217
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "group", "aria-label": `${headerLabel} options`, children: [
1218
+ options.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, opacity: 0.75 }, children: "No values" }),
1219
+ options.map((option) => {
1220
+ const checked = selectedSet.has(option.value);
1221
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1222
+ "label",
1223
+ {
1224
+ style: {
1225
+ display: "flex",
1226
+ alignItems: "center",
1227
+ gap: 8,
1228
+ padding: "4px 0",
1229
+ fontSize: 12
1230
+ },
1231
+ children: [
1232
+ /* @__PURE__ */ jsxRuntime.jsx(
1233
+ "input",
1234
+ {
1235
+ type: "checkbox",
1236
+ checked,
1237
+ onChange: () => toggleValue(option.value)
1238
+ }
1239
+ ),
1240
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { flex: 1 }, children: option.label }),
1241
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.6 }, children: option.count })
1242
+ ]
1243
+ },
1244
+ `${option.label}-${option.count}`
1245
+ );
1246
+ })
1247
+ ] })
1248
+ ]
1249
+ }
1250
+ )
1251
+ ]
1252
+ }
1253
+ );
1254
+ }
1255
+ function isSetCompatibleValue(value) {
1256
+ return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1257
+ }
1258
+ function inferFilterVariant(column) {
1259
+ const meta = column.columnDef.meta ?? {};
1260
+ if (meta.filterVariant) return meta.filterVariant;
1261
+ const uniqueValues = column.getFacetedUniqueValues();
1262
+ if (uniqueValues.size > 0 && uniqueValues.size <= 12) {
1263
+ const allValuesSupported = Array.from(uniqueValues.keys()).every(isSetCompatibleValue);
1264
+ if (allValuesSupported) return "set";
1265
+ }
1266
+ return "text";
1267
+ }
1268
+ function FloatingFilter({ column }) {
1269
+ const variant = inferFilterVariant(column);
1270
+ if (!column.getCanFilter()) {
1271
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { minHeight: 28 }, "aria-hidden": "true" });
1272
+ }
1273
+ if (variant === "set") {
1274
+ return /* @__PURE__ */ jsxRuntime.jsx(SetFilter, { column });
1275
+ }
1276
+ const filterValue = column.getFilterValue();
1277
+ const normalizedValue = typeof filterValue === "string" || typeof filterValue === "number" ? String(filterValue) : "";
1278
+ const label = typeof column.columnDef.header === "string" ? column.columnDef.header : column.id;
1279
+ return /* @__PURE__ */ jsxRuntime.jsx("label", { style: { display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1280
+ "input",
1281
+ {
1282
+ "aria-label": `Filter ${label}`,
1283
+ className: "yable-floating-filter-input",
1284
+ value: normalizedValue,
1285
+ onChange: (event) => {
1286
+ const nextValue = event.target.value;
1287
+ column.setFilterValue(nextValue === "" ? void 0 : nextValue);
1288
+ },
1289
+ placeholder: `Filter ${label}`,
1290
+ style: {
1291
+ width: "100%",
1292
+ minHeight: 28,
1293
+ padding: "4px 8px",
1294
+ border: "1px solid rgba(127, 127, 127, 0.35)",
1295
+ borderRadius: 6,
1296
+ background: "transparent",
1297
+ font: "inherit"
1298
+ }
1299
+ }
1300
+ ) });
1301
+ }
941
1302
  var DRAG_MIME = "application/yable-column";
942
1303
  function TableHeader({
943
- table
1304
+ table,
1305
+ floatingFilters = false
944
1306
  }) {
945
1307
  const headerGroups = table.getHeaderGroups();
946
- return /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "yable-thead", children: headerGroups.map((headerGroup) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsxRuntime.jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)) });
1308
+ const visibleColumns = table.getVisibleLeafColumns();
1309
+ return /* @__PURE__ */ jsxRuntime.jsxs("thead", { className: "yable-thead", children: [
1310
+ headerGroups.map((headerGroup) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "yable-header-row", children: headerGroup.headers.map((header) => /* @__PURE__ */ jsxRuntime.jsx(HeaderCell, { header, table }, header.id)) }, headerGroup.id)),
1311
+ floatingFilters && visibleColumns.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "yable-header-row yable-header-row--filters", children: visibleColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(FloatingFilterCell, { column }, `${column.id}-filter`)) })
1312
+ ] });
1313
+ }
1314
+ function FloatingFilterCell({
1315
+ column
1316
+ }) {
1317
+ const style = React3.useMemo(() => {
1318
+ const next = {
1319
+ width: column.getSize(),
1320
+ minWidth: column.columnDef.minSize,
1321
+ maxWidth: column.columnDef.maxSize,
1322
+ padding: 6,
1323
+ verticalAlign: "top"
1324
+ };
1325
+ const pinned = column.getIsPinned();
1326
+ if (pinned) {
1327
+ next.position = "sticky";
1328
+ if (pinned === "left") {
1329
+ next.left = column.getStart("left");
1330
+ } else {
1331
+ next.right = column.getStart("right");
1332
+ }
1333
+ }
1334
+ return next;
1335
+ }, [column]);
1336
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { className: "yable-th yable-th--filter", role: "columnheader", style, children: /* @__PURE__ */ jsxRuntime.jsx(FloatingFilter, { column }) });
947
1337
  }
948
1338
  function HeaderCell({
949
1339
  header,
@@ -1032,14 +1422,11 @@ function HeaderCell({
1032
1422
  },
1033
1423
  [canReorder]
1034
1424
  );
1035
- const handleDragLeave = React3.useCallback(
1036
- (e) => {
1037
- const next = e.relatedTarget;
1038
- if (next && e.currentTarget.contains(next)) return;
1039
- setDragOver(null);
1040
- },
1041
- []
1042
- );
1425
+ const handleDragLeave = React3.useCallback((e) => {
1426
+ const next = e.relatedTarget;
1427
+ if (next && e.currentTarget.contains(next)) return;
1428
+ setDragOver(null);
1429
+ }, []);
1043
1430
  const handleDragEnd = React3.useCallback(() => {
1044
1431
  setDragOver(null);
1045
1432
  }, []);
@@ -1112,13 +1499,7 @@ function HeaderCell({
1112
1499
  children: [
1113
1500
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yable-th-content", children: [
1114
1501
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: headerContent }),
1115
- canSort && /* @__PURE__ */ jsxRuntime.jsx(
1116
- SortIndicator,
1117
- {
1118
- direction: sortDirection,
1119
- index: sortIndex > 0 ? sortIndex : void 0
1120
- }
1121
- )
1502
+ canSort && /* @__PURE__ */ jsxRuntime.jsx(SortIndicator, { direction: sortDirection, index: sortIndex > 0 ? sortIndex : void 0 })
1122
1503
  ] }),
1123
1504
  canResize && /* @__PURE__ */ jsxRuntime.jsx(
1124
1505
  "div",
@@ -1243,6 +1624,10 @@ function TableCell({
1243
1624
  const cellStatus = table.getCellStatus(cell.row.id, column.id);
1244
1625
  const cellErrorMessage = table.getCellErrorMessage(cell.row.id, column.id);
1245
1626
  const cellConflictWith = table.getCellConflictWith(cell.row.id, column.id);
1627
+ const selectionRange = table.getCellSelectionRange();
1628
+ const selectionEdges = table.getCellSelectionEdges(rowIndex, columnIndex);
1629
+ const isCellSelected = selectionEdges !== null;
1630
+ const isMultiCellSelection = selectionRange !== null && (selectionRange.start.rowIndex !== selectionRange.end.rowIndex || selectionRange.start.columnIndex !== selectionRange.end.columnIndex);
1246
1631
  const overrideValue = cellStatus !== "idle" ? table.getCellRenderValue(cell.row.id, column.id) : void 0;
1247
1632
  let content;
1248
1633
  const cellDef = column.columnDef.cell;
@@ -1266,23 +1651,17 @@ function TableCell({
1266
1651
  }
1267
1652
  const handleClick = React3.useCallback(
1268
1653
  (e) => {
1269
- table.setFocusedCell({ rowIndex, columnIndex });
1270
- const clickTarget = e.target;
1271
- if (!isInteractiveClickTarget(clickTarget)) {
1272
- const currentTarget = e.currentTarget;
1273
- currentTarget.focus({ preventScroll: true });
1274
- }
1275
1654
  table.events.emit("cell:click", {
1276
1655
  cell,
1277
1656
  row: cell.row,
1278
1657
  column: cell.column,
1279
1658
  event: e.nativeEvent
1280
1659
  });
1281
- if (yableCore.canCellEnterEditMode(table, cell.row, column) && !isAlwaysEditable && !isEditing) {
1660
+ if (yableCore.canCellEnterEditMode(table, cell.row, column) && !isAlwaysEditable && !isEditing && !isMultiCellSelection && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
1282
1661
  table.startEditing(cell.row.id, column.id);
1283
1662
  }
1284
1663
  },
1285
- [cell, column, columnIndex, isAlwaysEditable, isEditing, rowIndex, table]
1664
+ [cell, column, isAlwaysEditable, isEditing, isMultiCellSelection, table]
1286
1665
  );
1287
1666
  const handleDoubleClick = React3.useCallback(
1288
1667
  (e) => {
@@ -1310,9 +1689,34 @@ function TableCell({
1310
1689
  if (!keyboardNavigationEnabled) return;
1311
1690
  table.setFocusedCell({ rowIndex, columnIndex });
1312
1691
  }, [columnIndex, keyboardNavigationEnabled, rowIndex, table]);
1692
+ const handleMouseDown = React3.useCallback(
1693
+ (e) => {
1694
+ if (e.button !== 0) return;
1695
+ const clickTarget = e.target;
1696
+ if (isInteractiveClickTarget(clickTarget)) return;
1697
+ e.preventDefault();
1698
+ table.setFocusedCell({ rowIndex, columnIndex });
1699
+ table.startCellRangeSelection({ rowIndex, columnIndex }, { extend: e.shiftKey });
1700
+ e.currentTarget.focus({ preventScroll: true });
1701
+ },
1702
+ [columnIndex, rowIndex, table]
1703
+ );
1704
+ const handleMouseEnter = React3.useCallback(() => {
1705
+ if (!table.getState().cellSelection?.isDragging) return;
1706
+ table.updateCellRangeSelection({ rowIndex, columnIndex });
1707
+ }, [columnIndex, rowIndex, table]);
1708
+ const handleMouseUp = React3.useCallback(() => {
1709
+ if (!table.getState().cellSelection?.isDragging) return;
1710
+ table.endCellRangeSelection();
1711
+ }, [table]);
1313
1712
  const classNames = [
1314
1713
  "yable-td",
1315
- isFocused && "yable-cell--focused"
1714
+ isFocused && "yable-cell--focused",
1715
+ isCellSelected && "yable-cell--selected",
1716
+ selectionEdges?.top && "yable-cell--selection-top",
1717
+ selectionEdges?.right && "yable-cell--selection-right",
1718
+ selectionEdges?.bottom && "yable-cell--selection-bottom",
1719
+ selectionEdges?.left && "yable-cell--selection-left"
1316
1720
  ].filter(Boolean).join(" ");
1317
1721
  return /* @__PURE__ */ jsxRuntime.jsxs(
1318
1722
  "td",
@@ -1326,10 +1730,14 @@ function TableCell({
1326
1730
  "data-column-id": column.id,
1327
1731
  "data-row-index": rowIndex,
1328
1732
  "data-column-index": columnIndex,
1733
+ "data-cell-selected": isCellSelected || void 0,
1329
1734
  "aria-rowindex": rowIndex + 1,
1330
1735
  "aria-colindex": columnIndex + 1,
1331
1736
  role: "gridcell",
1332
1737
  tabIndex: keyboardNavigationEnabled ? isTabStop ? 0 : -1 : void 0,
1738
+ onMouseDown: handleMouseDown,
1739
+ onMouseEnter: handleMouseEnter,
1740
+ onMouseUp: handleMouseUp,
1333
1741
  onClick: handleClick,
1334
1742
  onDoubleClick: handleDoubleClick,
1335
1743
  onContextMenu: handleContextMenu,
@@ -1454,14 +1862,15 @@ var CellErrorBoundary = class extends React3__default.default.Component {
1454
1862
  return this.props.children;
1455
1863
  }
1456
1864
  };
1457
- function TableBody({
1458
- table,
1459
- clickableRows
1460
- }) {
1865
+ function TableBody({ table, clickableRows }) {
1461
1866
  const rows = table.getRowModel().rows;
1462
1867
  const visibleColumns = table.getVisibleLeafColumns();
1463
1868
  const activeCell = table.getState().editing.activeCell;
1464
1869
  const focusedCell = table.getFocusedCell();
1870
+ const cellSelection = table.getState().cellSelection ?? {
1871
+ range: null,
1872
+ isDragging: false
1873
+ };
1465
1874
  const options = table.options;
1466
1875
  const enableVirtualization = options.enableVirtualization ?? false;
1467
1876
  const scrollContainerRef = React3.useRef(null);
@@ -1479,6 +1888,18 @@ function TableBody({
1479
1888
  pretextHeights,
1480
1889
  pretextPrefixSums
1481
1890
  });
1891
+ 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"}`;
1892
+ React3.useEffect(() => {
1893
+ const handleWindowMouseUp = () => {
1894
+ if (table.getState().cellSelection?.isDragging) {
1895
+ table.endCellRangeSelection();
1896
+ }
1897
+ };
1898
+ window.addEventListener("mouseup", handleWindowMouseUp);
1899
+ return () => {
1900
+ window.removeEventListener("mouseup", handleWindowMouseUp);
1901
+ };
1902
+ }, [table]);
1482
1903
  if (!enableVirtualization) {
1483
1904
  return /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "yable-tbody", children: rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
1484
1905
  MemoizedTableRow,
@@ -1492,6 +1913,7 @@ function TableBody({
1492
1913
  activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1493
1914
  focusedColumnIndex: focusedCell?.rowIndex === rowIndex ? focusedCell.columnIndex : null,
1494
1915
  hasFocusedCell: focusedCell !== null,
1916
+ cellSelectionKey,
1495
1917
  clickable: clickableRows
1496
1918
  },
1497
1919
  row.id
@@ -1500,73 +1922,67 @@ function TableBody({
1500
1922
  const hasPretextData = !!(pretextHeights && pretextPrefixSums);
1501
1923
  const fixedRowHeight = typeof rowHeight === "number" && !hasPretextData ? rowHeight : void 0;
1502
1924
  const containerHeight = hasPretextData ? Math.min(totalHeight, 800) : fixedRowHeight ? Math.min(totalHeight, fixedRowHeight * 20) : 600;
1503
- return /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "yable-tbody", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { style: { height: 0, padding: 0, border: "none" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1504
- "td",
1925
+ return /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "yable-tbody", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { style: { height: 0, padding: 0, border: "none" }, children: /* @__PURE__ */ jsxRuntime.jsx("td", { style: { height: 0, padding: 0, border: "none" }, colSpan: visibleColumns.length, children: /* @__PURE__ */ jsxRuntime.jsx(
1926
+ "div",
1505
1927
  {
1506
- style: { height: 0, padding: 0, border: "none" },
1507
- colSpan: visibleColumns.length,
1928
+ ref: scrollContainerRef,
1929
+ className: "yable-virtual-scroll-container",
1930
+ style: {
1931
+ overflowY: "auto",
1932
+ height: containerHeight,
1933
+ position: "relative"
1934
+ },
1508
1935
  children: /* @__PURE__ */ jsxRuntime.jsx(
1509
1936
  "div",
1510
1937
  {
1511
- ref: scrollContainerRef,
1512
- className: "yable-virtual-scroll-container",
1513
- style: {
1514
- overflowY: "auto",
1515
- height: containerHeight,
1516
- position: "relative"
1517
- },
1938
+ className: "yable-virtual-spacer",
1939
+ style: { height: totalHeight, position: "relative" },
1518
1940
  children: /* @__PURE__ */ jsxRuntime.jsx(
1519
- "div",
1941
+ "table",
1520
1942
  {
1521
- className: "yable-virtual-spacer",
1522
- style: { height: totalHeight, position: "relative" },
1523
- children: /* @__PURE__ */ jsxRuntime.jsx(
1524
- "table",
1525
- {
1526
- style: {
1527
- position: "absolute",
1528
- top: 0,
1529
- left: 0,
1530
- width: "100%",
1531
- tableLayout: "fixed",
1532
- borderCollapse: "collapse"
1943
+ style: {
1944
+ position: "absolute",
1945
+ top: 0,
1946
+ left: 0,
1947
+ width: "100%",
1948
+ tableLayout: "fixed",
1949
+ borderCollapse: "collapse"
1950
+ },
1951
+ children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: virtualRows.map((vRow) => {
1952
+ const row = rows[vRow.index];
1953
+ if (!row) return null;
1954
+ return /* @__PURE__ */ jsxRuntime.jsx(
1955
+ MemoizedTableRow,
1956
+ {
1957
+ row,
1958
+ table,
1959
+ rowIndex: vRow.index,
1960
+ visibleColumns,
1961
+ isSelected: row.getIsSelected(),
1962
+ isExpanded: row.getIsExpanded(),
1963
+ activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1964
+ focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
1965
+ hasFocusedCell: focusedCell !== null,
1966
+ cellSelectionKey,
1967
+ clickable: clickableRows,
1968
+ virtualStyle: {
1969
+ position: "absolute",
1970
+ top: 0,
1971
+ left: 0,
1972
+ width: "100%",
1973
+ height: vRow.size,
1974
+ transform: `translateY(${vRow.start}px)`
1975
+ }
1533
1976
  },
1534
- children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: virtualRows.map((vRow) => {
1535
- const row = rows[vRow.index];
1536
- if (!row) return null;
1537
- return /* @__PURE__ */ jsxRuntime.jsx(
1538
- MemoizedTableRow,
1539
- {
1540
- row,
1541
- table,
1542
- rowIndex: vRow.index,
1543
- visibleColumns,
1544
- isSelected: row.getIsSelected(),
1545
- isExpanded: row.getIsExpanded(),
1546
- activeColumnId: activeCell?.rowId === row.id ? activeCell.columnId : void 0,
1547
- focusedColumnIndex: focusedCell?.rowIndex === vRow.index ? focusedCell.columnIndex : null,
1548
- hasFocusedCell: focusedCell !== null,
1549
- clickable: clickableRows,
1550
- virtualStyle: {
1551
- position: "absolute",
1552
- top: 0,
1553
- left: 0,
1554
- width: "100%",
1555
- height: vRow.size,
1556
- transform: `translateY(${vRow.start}px)`
1557
- }
1558
- },
1559
- row.id
1560
- );
1561
- }) })
1562
- }
1563
- )
1977
+ row.id
1978
+ );
1979
+ }) })
1564
1980
  }
1565
1981
  )
1566
1982
  }
1567
1983
  )
1568
1984
  }
1569
- ) }) });
1985
+ ) }) }) });
1570
1986
  }
1571
1987
  function TableRowInner({
1572
1988
  row,
@@ -1578,10 +1994,12 @@ function TableRowInner({
1578
1994
  activeColumnId,
1579
1995
  focusedColumnIndex,
1580
1996
  hasFocusedCell,
1997
+ cellSelectionKey: _cellSelectionKey,
1581
1998
  clickable,
1582
1999
  virtualStyle
1583
2000
  }) {
1584
- const visibleCells = row.getVisibleCells();
2001
+ const allCells = row.getAllCells();
2002
+ const visibleCells = visibleColumns.map((column) => allCells.find((cell) => cell.column.id === column.id)).filter((cell) => cell != null);
1585
2003
  const handleClick = React3.useCallback(
1586
2004
  (e) => {
1587
2005
  if (clickable) {
@@ -1611,6 +2029,8 @@ function TableRowInner({
1611
2029
  },
1612
2030
  [table.events, row]
1613
2031
  );
2032
+ const selectionEnabled = Boolean(table.options.enableRowSelection);
2033
+ const expansionEnabled = Boolean(table.options.enableExpanding);
1614
2034
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1615
2035
  /* @__PURE__ */ jsxRuntime.jsx(
1616
2036
  "tr",
@@ -1622,6 +2042,8 @@ function TableRowInner({
1622
2042
  "data-clickable": clickable || void 0,
1623
2043
  "data-row-id": row.id,
1624
2044
  "data-row-index": rowIndex,
2045
+ "aria-selected": selectionEnabled ? isSelected : void 0,
2046
+ "aria-expanded": expansionEnabled ? isExpanded : void 0,
1625
2047
  onClick: handleClick,
1626
2048
  onDoubleClick: handleDoubleClick,
1627
2049
  onContextMenu: handleContextMenu,
@@ -1663,6 +2085,7 @@ function areRowPropsEqual(prev, next) {
1663
2085
  if (prev.activeColumnId !== next.activeColumnId) return false;
1664
2086
  if (prev.focusedColumnIndex !== next.focusedColumnIndex) return false;
1665
2087
  if (prev.hasFocusedCell !== next.hasFocusedCell) return false;
2088
+ if (prev.cellSelectionKey !== next.cellSelectionKey) return false;
1666
2089
  if (prev.virtualStyle !== next.virtualStyle) {
1667
2090
  if (!prev.virtualStyle || !next.virtualStyle) return false;
1668
2091
  if (prev.virtualStyle.transform !== next.virtualStyle.transform) return false;
@@ -1672,9 +2095,7 @@ function areRowPropsEqual(prev, next) {
1672
2095
  return true;
1673
2096
  }
1674
2097
  var MemoizedTableRow = React3__default.default.memo(TableRowInner, areRowPropsEqual);
1675
- function TableFooter({
1676
- table
1677
- }) {
2098
+ function TableFooter({ table }) {
1678
2099
  const footerGroups = table.getFooterGroups();
1679
2100
  if (!footerGroups.length) return null;
1680
2101
  return /* @__PURE__ */ jsxRuntime.jsx("tfoot", { className: "yable-tfoot", children: footerGroups.map((footerGroup) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "yable-tr", children: footerGroup.headers.map((header) => {
@@ -1720,19 +2141,20 @@ function Spinner() {
1720
2141
  function LoadingOverlay({
1721
2142
  loading,
1722
2143
  loadingComponent,
1723
- loadingText = "Loading..."
2144
+ loadingText
1724
2145
  }) {
1725
2146
  if (!loading) return null;
2147
+ const resolvedText = loadingText ?? yableCore.getDefaultLocale().loadingText;
1726
2148
  return /* @__PURE__ */ jsxRuntime.jsx(
1727
2149
  "div",
1728
2150
  {
1729
2151
  className: "yable-overlay-loading",
1730
2152
  role: "alert",
1731
2153
  "aria-busy": "true",
1732
- "aria-label": loadingText,
2154
+ "aria-label": resolvedText,
1733
2155
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "yable-overlay-loading-content", children: loadingComponent ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1734
2156
  /* @__PURE__ */ jsxRuntime.jsx(Spinner, {}),
1735
- loadingText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "yable-overlay-loading-text", children: loadingText })
2157
+ resolvedText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "yable-overlay-loading-text", children: resolvedText })
1736
2158
  ] }) })
1737
2159
  }
1738
2160
  );
@@ -1850,8 +2272,9 @@ function NoRowsOverlay({
1850
2272
  if (emptyComponent) {
1851
2273
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "yable-overlay-empty", children: emptyComponent });
1852
2274
  }
1853
- const defaultMessage = isFiltered ? "No results found" : "No data";
1854
- const defaultDetail = isFiltered ? "Try adjusting your search or filter criteria." : "There are no rows to display.";
2275
+ const locale = yableCore.getDefaultLocale();
2276
+ const defaultMessage = isFiltered ? locale.emptyNoResults : locale.emptyNoData;
2277
+ const defaultDetail = isFiltered ? locale.emptyNoResultsDetail : locale.emptyNoDataDetail;
1855
2278
  const icon = emptyIcon ?? (isFiltered ? /* @__PURE__ */ jsxRuntime.jsx(DefaultFilteredIcon, {}) : /* @__PURE__ */ jsxRuntime.jsx(DefaultEmptyIcon, {}));
1856
2279
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yable-overlay-empty", role: "status", children: [
1857
2280
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "yable-overlay-empty-icon-wrapper", children: icon }),
@@ -2435,11 +2858,11 @@ function ContextMenu({
2435
2858
  }
2436
2859
  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2437
2860
  e.preventDefault();
2438
- const items2 = menuRef.current?.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])');
2439
- if (!items2 || items2.length === 0) return;
2440
- const currentIndex = Array.from(items2).findIndex(
2441
- (el) => el === document.activeElement
2861
+ const items2 = menuRef.current?.querySelectorAll(
2862
+ '[role="menuitem"]:not([aria-disabled="true"])'
2442
2863
  );
2864
+ if (!items2 || items2.length === 0) return;
2865
+ const currentIndex = Array.from(items2).findIndex((el) => el === document.activeElement);
2443
2866
  let nextIndex;
2444
2867
  if (e.key === "ArrowDown") {
2445
2868
  nextIndex = currentIndex < items2.length - 1 ? currentIndex + 1 : 0;
@@ -2484,11 +2907,7 @@ function ContextMenu({
2484
2907
  navigator.clipboard?.readText().then((text) => {
2485
2908
  const editing = table.getState().editing;
2486
2909
  if (editing?.activeCell) {
2487
- table.pasteFromClipboard(
2488
- text,
2489
- editing.activeCell.rowId,
2490
- editing.activeCell.columnId
2491
- );
2910
+ table.pasteFromClipboard(text, editing.activeCell.rowId, editing.activeCell.columnId);
2492
2911
  }
2493
2912
  }).catch(() => {
2494
2913
  });
@@ -2828,6 +3247,12 @@ function isEditableTarget(element) {
2828
3247
  }
2829
3248
  return element.isContentEditable;
2830
3249
  }
3250
+ function filterHeaderGroups(groups, visibleColumnIds) {
3251
+ return groups.map((group) => ({
3252
+ ...group,
3253
+ headers: group.headers.filter((header) => visibleColumnIds.has(header.column.id))
3254
+ })).filter((group) => group.headers.length > 0);
3255
+ }
2831
3256
  function Table({
2832
3257
  table,
2833
3258
  stickyHeader,
@@ -2840,7 +3265,7 @@ function Table({
2840
3265
  loading,
2841
3266
  loadingComponent,
2842
3267
  loadingText,
2843
- emptyMessage = "No data",
3268
+ emptyMessage,
2844
3269
  emptyComponent,
2845
3270
  emptyIcon,
2846
3271
  emptyDetail,
@@ -2854,6 +3279,10 @@ function Table({
2854
3279
  sidebar,
2855
3280
  sidebarPanels = ["columns", "filters"],
2856
3281
  defaultSidebarPanel,
3282
+ floatingFilters,
3283
+ columnVirtualization,
3284
+ columnVirtualizationOverscan,
3285
+ ariaLabel,
2857
3286
  ...rest
2858
3287
  }) {
2859
3288
  const [sidebarOpen, setSidebarOpen] = React3.useState(false);
@@ -2861,6 +3290,7 @@ function Table({
2861
3290
  defaultSidebarPanel ?? "columns"
2862
3291
  );
2863
3292
  const containerRef = React3.useRef(null);
3293
+ const horizontalScrollRef = React3.useRef(null);
2864
3294
  const isRtl = direction === "rtl";
2865
3295
  const classNames = [
2866
3296
  "yable",
@@ -2878,8 +3308,86 @@ function Table({
2878
3308
  const hasGlobalFilter = Boolean(table.getState().globalFilter);
2879
3309
  const hasColumnFilters = table.getState().columnFilters.length > 0;
2880
3310
  const isFiltered = hasGlobalFilter || hasColumnFilters;
3311
+ const allVisibleColumns = table.getVisibleLeafColumns();
3312
+ const hasPinnedColumns = table.getLeftVisibleLeafColumns().length > 0 || table.getRightVisibleLeafColumns().length > 0;
3313
+ const hasGroupedHeaders = table.getHeaderGroups().length > 1;
3314
+ const canVirtualizeColumns = Boolean(columnVirtualization) && !hasPinnedColumns && !hasGroupedHeaders && allVisibleColumns.length > 0;
3315
+ const columnVirtualState = useColumnVirtualization({
3316
+ containerRef: horizontalScrollRef,
3317
+ columns: allVisibleColumns,
3318
+ overscan: columnVirtualizationOverscan ?? 2,
3319
+ enabled: canVirtualizeColumns
3320
+ });
3321
+ const renderTable = React3.useMemo(() => {
3322
+ if (!canVirtualizeColumns || !columnVirtualState.isVirtualized) {
3323
+ return table;
3324
+ }
3325
+ const virtualColumns = columnVirtualState.virtualColumns.map((entry) => entry.column);
3326
+ const visibleColumnIds = new Set(virtualColumns.map((column) => column.id));
3327
+ const next = Object.create(table);
3328
+ next.getVisibleFlatColumns = () => virtualColumns;
3329
+ next.getVisibleLeafColumns = () => virtualColumns;
3330
+ next.getCenterVisibleLeafColumns = () => virtualColumns;
3331
+ next.getLeftVisibleLeafColumns = () => [];
3332
+ next.getRightVisibleLeafColumns = () => [];
3333
+ next.getHeaderGroups = () => filterHeaderGroups(table.getHeaderGroups(), visibleColumnIds);
3334
+ next.getCenterHeaderGroups = () => filterHeaderGroups(table.getCenterHeaderGroups(), visibleColumnIds);
3335
+ next.getFooterGroups = () => filterHeaderGroups(table.getFooterGroups(), visibleColumnIds);
3336
+ next.getCenterFooterGroups = () => filterHeaderGroups(table.getCenterFooterGroups(), visibleColumnIds);
3337
+ return next;
3338
+ }, [
3339
+ canVirtualizeColumns,
3340
+ columnVirtualState.isVirtualized,
3341
+ columnVirtualState.virtualColumns,
3342
+ table
3343
+ ]);
3344
+ const showColumnVirtualizationShell = canVirtualizeColumns;
2881
3345
  const contextMenu = useContextMenu();
2882
3346
  useKeyboardNavigation(table, { containerRef });
3347
+ const [announcement, setAnnouncement] = React3.useState("");
3348
+ const prevSortingRef = React3.useRef(table.getState().sorting);
3349
+ const prevFilterCountRef = React3.useRef(rows.length);
3350
+ const prevHasFiltersRef = React3.useRef(isFiltered);
3351
+ const prevPaginationRef = React3.useRef(table.getState().pagination);
3352
+ const isFirstRenderRef = React3.useRef(true);
3353
+ React3.useEffect(() => {
3354
+ if (isFirstRenderRef.current) {
3355
+ isFirstRenderRef.current = false;
3356
+ return;
3357
+ }
3358
+ const currentSorting = table.getState().sorting;
3359
+ const currentPagination = table.getState().pagination;
3360
+ const currentRowCount = rows.length;
3361
+ const currentIsFiltered = isFiltered;
3362
+ if (JSON.stringify(currentSorting) !== JSON.stringify(prevSortingRef.current)) {
3363
+ prevSortingRef.current = currentSorting;
3364
+ const firstSort = currentSorting[0];
3365
+ if (firstSort) {
3366
+ const column = table.getColumn(firstSort.id);
3367
+ const headerDef = column?.columnDef.header;
3368
+ const columnName = typeof headerDef === "string" ? headerDef : firstSort.id;
3369
+ const direction2 = firstSort.desc ? "descending" : "ascending";
3370
+ setAnnouncement(`Sorted by ${columnName} ${direction2}`);
3371
+ return;
3372
+ }
3373
+ }
3374
+ if (currentRowCount !== prevFilterCountRef.current || currentIsFiltered !== prevHasFiltersRef.current) {
3375
+ prevFilterCountRef.current = currentRowCount;
3376
+ prevHasFiltersRef.current = currentIsFiltered;
3377
+ if (currentIsFiltered) {
3378
+ setAnnouncement(`${currentRowCount} rows after filtering`);
3379
+ return;
3380
+ }
3381
+ }
3382
+ if (JSON.stringify(currentPagination) !== JSON.stringify(prevPaginationRef.current)) {
3383
+ prevPaginationRef.current = currentPagination;
3384
+ const pageCount = table.getPageCount();
3385
+ if (pageCount > 0) {
3386
+ setAnnouncement(`Page ${currentPagination.pageIndex + 1} of ${pageCount}`);
3387
+ return;
3388
+ }
3389
+ }
3390
+ });
2883
3391
  const handleContextMenu = React3.useCallback(
2884
3392
  (e) => {
2885
3393
  e.preventDefault();
@@ -2887,6 +3395,24 @@ function Table({
2887
3395
  },
2888
3396
  [contextMenu, table]
2889
3397
  );
3398
+ const tableNode = /* @__PURE__ */ jsxRuntime.jsxs(
3399
+ "table",
3400
+ {
3401
+ className: "yable-table",
3402
+ style: columnVirtualState.isVirtualized ? {
3403
+ width: columnVirtualState.visibleWidth,
3404
+ minWidth: columnVirtualState.visibleWidth,
3405
+ marginLeft: columnVirtualState.startOffset,
3406
+ tableLayout: "fixed"
3407
+ } : void 0,
3408
+ "data-column-virtualized": columnVirtualState.isVirtualized || void 0,
3409
+ children: [
3410
+ /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { table: renderTable, floatingFilters }),
3411
+ /* @__PURE__ */ jsxRuntime.jsx(TableBody, { table: renderTable, clickableRows }),
3412
+ footer && /* @__PURE__ */ jsxRuntime.jsx(TableFooter, { table: renderTable })
3413
+ ]
3414
+ }
3415
+ );
2890
3416
  return /* @__PURE__ */ jsxRuntime.jsx(TableProvider, { value: table, children: /* @__PURE__ */ jsxRuntime.jsxs(
2891
3417
  "div",
2892
3418
  {
@@ -2895,23 +3421,40 @@ function Table({
2895
3421
  "data-theme": theme,
2896
3422
  dir: direction,
2897
3423
  role: "grid",
3424
+ "aria-label": ariaLabel ?? "Data table",
2898
3425
  "aria-rowcount": table.getRowModel().rows.length,
2899
3426
  "aria-colcount": table.getVisibleLeafColumns().length,
2900
3427
  onContextMenu: handleContextMenu,
2901
3428
  ...rest,
2902
3429
  children: [
2903
3430
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yable-main", children: [
2904
- /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "yable-table", children: [
2905
- /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { table }),
2906
- /* @__PURE__ */ jsxRuntime.jsx(
2907
- TableBody,
2908
- {
2909
- table,
2910
- clickableRows
2911
- }
2912
- ),
2913
- footer && /* @__PURE__ */ jsxRuntime.jsx(TableFooter, { table })
2914
- ] }),
3431
+ showColumnVirtualizationShell ? /* @__PURE__ */ jsxRuntime.jsx(
3432
+ "div",
3433
+ {
3434
+ ref: horizontalScrollRef,
3435
+ className: "yable-horizontal-scroll-container",
3436
+ style: {
3437
+ overflowX: "auto",
3438
+ overflowY: "visible",
3439
+ maxWidth: "100%",
3440
+ position: "relative"
3441
+ },
3442
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3443
+ "div",
3444
+ {
3445
+ className: "yable-horizontal-scroll-inner",
3446
+ style: {
3447
+ width: Math.max(columnVirtualState.totalWidth, columnVirtualState.visibleWidth),
3448
+ minWidth: Math.max(
3449
+ columnVirtualState.totalWidth,
3450
+ columnVirtualState.visibleWidth
3451
+ )
3452
+ },
3453
+ children: tableNode
3454
+ }
3455
+ )
3456
+ }
3457
+ ) : tableNode,
2915
3458
  /* @__PURE__ */ jsxRuntime.jsx(
2916
3459
  LoadingOverlay,
2917
3460
  {
@@ -2952,6 +3495,24 @@ function Table({
2952
3495
  onClose: contextMenu.close,
2953
3496
  table
2954
3497
  }
3498
+ ),
3499
+ /* @__PURE__ */ jsxRuntime.jsx(
3500
+ "div",
3501
+ {
3502
+ role: "status",
3503
+ "aria-live": "polite",
3504
+ "aria-atomic": "true",
3505
+ style: {
3506
+ position: "absolute",
3507
+ width: "1px",
3508
+ height: "1px",
3509
+ overflow: "hidden",
3510
+ clip: "rect(0,0,0,0)",
3511
+ whiteSpace: "nowrap",
3512
+ border: 0
3513
+ },
3514
+ children: announcement
3515
+ }
2955
3516
  )
2956
3517
  ]
2957
3518
  }
@@ -2992,9 +3553,10 @@ function Pagination({
2992
3553
  const to = Math.min((pageIndex + 1) * pageSize, totalRows);
2993
3554
  const canPrev = table.getCanPreviousPage();
2994
3555
  const canNext = table.getCanNextPage();
3556
+ const locale = yableCore.getDefaultLocale();
2995
3557
  return /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "yable-pagination", role: "navigation", "aria-label": "Table pagination", children: [
2996
3558
  showInfo && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yable-pagination-info", children: [
2997
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "yable-pagination-info-text", children: totalRows > 0 ? `${from}\u2013${to} of ${totalRows}` : "No results" }),
3559
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "yable-pagination-info-text", children: totalRows > 0 ? `${from}\u2013${to} ${locale.paginationOf} ${totalRows}` : locale.paginationNoResults }),
2998
3560
  showPageSize && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yable-pagination-select-wrapper", children: [
2999
3561
  /* @__PURE__ */ jsxRuntime.jsx(
3000
3562
  "select",
@@ -3007,7 +3569,8 @@ function Pagination({
3007
3569
  "aria-label": "Rows per page",
3008
3570
  children: pageSizes.map((size) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: size, children: [
3009
3571
  size,
3010
- " rows"
3572
+ " ",
3573
+ locale.paginationRows
3011
3574
  ] }, size))
3012
3575
  }
3013
3576
  ),
@@ -3022,8 +3585,8 @@ function Pagination({
3022
3585
  className: "yable-pagination-btn yable-pagination-btn--nav",
3023
3586
  onClick: () => table.setPageIndex(0),
3024
3587
  disabled: !canPrev,
3025
- "aria-label": "First page",
3026
- title: "First page",
3588
+ "aria-label": locale.paginationFirstPage,
3589
+ title: locale.paginationFirstPage,
3027
3590
  children: /* @__PURE__ */ jsxRuntime.jsx(ChevronFirstIcon, {})
3028
3591
  }
3029
3592
  ),
@@ -3034,8 +3597,8 @@ function Pagination({
3034
3597
  className: "yable-pagination-btn yable-pagination-btn--nav",
3035
3598
  onClick: () => table.previousPage(),
3036
3599
  disabled: !canPrev,
3037
- "aria-label": "Previous page",
3038
- title: "Previous page",
3600
+ "aria-label": locale.paginationPreviousPage,
3601
+ title: locale.paginationPreviousPage,
3039
3602
  children: /* @__PURE__ */ jsxRuntime.jsx(ChevronLeftIcon, {})
3040
3603
  }
3041
3604
  ),
@@ -3059,7 +3622,7 @@ function Pagination({
3059
3622
  className: `yable-pagination-btn yable-pagination-btn--page${page === pageIndex ? " yable-pagination-btn--active" : ""}`,
3060
3623
  "data-active": page === pageIndex ? "true" : void 0,
3061
3624
  onClick: () => table.setPageIndex(page),
3062
- "aria-label": `Page ${page + 1}`,
3625
+ "aria-label": `${locale.paginationPage} ${page + 1}`,
3063
3626
  "aria-current": page === pageIndex ? "page" : void 0,
3064
3627
  children: page + 1
3065
3628
  },
@@ -3073,8 +3636,8 @@ function Pagination({
3073
3636
  className: "yable-pagination-btn yable-pagination-btn--nav",
3074
3637
  onClick: () => table.nextPage(),
3075
3638
  disabled: !canNext,
3076
- "aria-label": "Next page",
3077
- title: "Next page",
3639
+ "aria-label": locale.paginationNextPage,
3640
+ title: locale.paginationNextPage,
3078
3641
  children: /* @__PURE__ */ jsxRuntime.jsx(ChevronRightIcon, {})
3079
3642
  }
3080
3643
  ),
@@ -3085,8 +3648,8 @@ function Pagination({
3085
3648
  className: "yable-pagination-btn yable-pagination-btn--nav",
3086
3649
  onClick: () => table.setPageIndex(pageCount - 1),
3087
3650
  disabled: !canNext,
3088
- "aria-label": "Last page",
3089
- title: "Last page",
3651
+ "aria-label": locale.paginationLastPage,
3652
+ title: locale.paginationLastPage,
3090
3653
  children: /* @__PURE__ */ jsxRuntime.jsx(ChevronLastIcon, {})
3091
3654
  }
3092
3655
  )
@@ -3135,10 +3698,12 @@ function ClearIcon() {
3135
3698
  }
3136
3699
  function GlobalFilter({
3137
3700
  table,
3138
- placeholder = "Search...",
3701
+ placeholder,
3139
3702
  debounce = 300,
3140
3703
  className
3141
3704
  }) {
3705
+ const locale = yableCore.getDefaultLocale();
3706
+ const resolvedPlaceholder = placeholder ?? locale.searchPlaceholder;
3142
3707
  const [value, setValue] = React3.useState(
3143
3708
  table.getState().globalFilter ?? ""
3144
3709
  );
@@ -3195,8 +3760,8 @@ function GlobalFilter({
3195
3760
  value,
3196
3761
  onChange: handleChange,
3197
3762
  onKeyDown: handleKeyDown,
3198
- placeholder,
3199
- "aria-label": "Search table",
3763
+ placeholder: resolvedPlaceholder,
3764
+ "aria-label": locale.searchAriaLabel,
3200
3765
  role: "searchbox"
3201
3766
  }
3202
3767
  ),
@@ -3283,10 +3848,25 @@ function CellSelect({
3283
3848
  const isAlwaysEditable = cell.getIsAlwaysEditable();
3284
3849
  const pending = table.getPendingValue(row.id, column.id);
3285
3850
  const currentValue = pending !== void 0 ? pending : cell.getValue();
3851
+ const commitTimerRef = React3.useRef(null);
3852
+ React3.useEffect(() => {
3853
+ return () => {
3854
+ if (commitTimerRef.current !== null) {
3855
+ clearTimeout(commitTimerRef.current);
3856
+ commitTimerRef.current = null;
3857
+ }
3858
+ };
3859
+ }, []);
3286
3860
  const handleChange = (e) => {
3287
3861
  table.setPendingValue(row.id, column.id, e.target.value);
3288
3862
  if (isEditing && !isAlwaysEditable) {
3289
- setTimeout(() => table.commitEdit(), 0);
3863
+ if (commitTimerRef.current !== null) {
3864
+ clearTimeout(commitTimerRef.current);
3865
+ }
3866
+ commitTimerRef.current = setTimeout(() => {
3867
+ commitTimerRef.current = null;
3868
+ table.commitEdit();
3869
+ }, 0);
3290
3870
  }
3291
3871
  };
3292
3872
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3398,14 +3978,7 @@ function formatDateValue(value, type) {
3398
3978
  return String(value);
3399
3979
  }
3400
3980
  function useClipboard(table, options = {}) {
3401
- const {
3402
- enabled = true,
3403
- containerRef,
3404
- onCopy,
3405
- onCut,
3406
- onPaste,
3407
- ...clipboardOptions
3408
- } = options;
3981
+ const { enabled = true, containerRef, onCopy, onCut, onPaste, ...clipboardOptions } = options;
3409
3982
  const handleCopy = React3.useCallback(
3410
3983
  (e) => {
3411
3984
  if (isEditableTarget2(e.target)) return;
@@ -3443,20 +4016,29 @@ function useClipboard(table, options = {}) {
3443
4016
  targetRowId = state.editing.activeCell.rowId;
3444
4017
  targetColumnId = state.editing.activeCell.columnId;
3445
4018
  } else {
3446
- const selectedRowIds = Object.keys(state.rowSelection || {}).filter(
3447
- (id) => state.rowSelection[id]
3448
- );
3449
- if (selectedRowIds.length > 0) {
3450
- targetRowId = selectedRowIds[0];
4019
+ const selectedRange = table.getCellSelectionRange();
4020
+ const rows = table.getRowModel().rows;
4021
+ const columns = table.getVisibleLeafColumns();
4022
+ if (selectedRange) {
4023
+ const startRowIndex = Math.min(selectedRange.start.rowIndex, selectedRange.end.rowIndex);
4024
+ const startColumnIndex = Math.min(
4025
+ selectedRange.start.columnIndex,
4026
+ selectedRange.end.columnIndex
4027
+ );
4028
+ targetRowId = rows[startRowIndex]?.id;
4029
+ targetColumnId = columns[startColumnIndex]?.id;
3451
4030
  } else {
3452
- const rows = table.getRowModel().rows;
3453
- if (rows.length > 0) {
4031
+ const selectedRowIds = Object.keys(state.rowSelection || {}).filter(
4032
+ (id) => state.rowSelection[id]
4033
+ );
4034
+ if (selectedRowIds.length > 0) {
4035
+ targetRowId = selectedRowIds[0];
4036
+ } else if (rows.length > 0) {
3454
4037
  targetRowId = rows[0].id;
3455
4038
  }
3456
- }
3457
- const columns = table.getVisibleLeafColumns();
3458
- if (columns.length > 0) {
3459
- targetColumnId = columns[0].id;
4039
+ if (columns.length > 0) {
4040
+ targetColumnId = columns[0].id;
4041
+ }
3460
4042
  }
3461
4043
  }
3462
4044
  if (targetRowId && targetColumnId) {
@@ -4746,7 +5328,7 @@ function sanitizeCSS(css) {
4746
5328
  sanitized = sanitized.replace(/javascript\s*:/gi, "/* javascript: removed */");
4747
5329
  return sanitized;
4748
5330
  }
4749
- function usePrintLayout(table, options = {}) {
5331
+ function usePrintLayout(_table, options = {}) {
4750
5332
  const { title, additionalCSS } = options;
4751
5333
  const isPrintingRef = React3.useRef(false);
4752
5334
  const preparePrint = React3.useCallback(() => {
@@ -4783,7 +5365,7 @@ function usePrintLayout(table, options = {}) {
4783
5365
  requestAnimationFrame(() => {
4784
5366
  window.print();
4785
5367
  });
4786
- }, [table, title, additionalCSS]);
5368
+ }, [title, additionalCSS]);
4787
5369
  return {
4788
5370
  preparePrint,
4789
5371
  isPrinting: isPrintingRef.current
@@ -4839,6 +5421,22 @@ Object.defineProperty(exports, "CommitError", {
4839
5421
  enumerable: true,
4840
5422
  get: function () { return yableCore.CommitError; }
4841
5423
  });
5424
+ Object.defineProperty(exports, "FormulaEngine", {
5425
+ enumerable: true,
5426
+ get: function () { return yableCore.FormulaEngine; }
5427
+ });
5428
+ Object.defineProperty(exports, "FormulaError", {
5429
+ enumerable: true,
5430
+ get: function () { return yableCore.FormulaError; }
5431
+ });
5432
+ Object.defineProperty(exports, "PivotEngine", {
5433
+ enumerable: true,
5434
+ get: function () { return yableCore.PivotEngine; }
5435
+ });
5436
+ Object.defineProperty(exports, "UndoStack", {
5437
+ enumerable: true,
5438
+ get: function () { return yableCore.UndoStack; }
5439
+ });
4842
5440
  Object.defineProperty(exports, "aggregationFns", {
4843
5441
  enumerable: true,
4844
5442
  get: function () { return yableCore.aggregationFns; }
@@ -4851,6 +5449,10 @@ Object.defineProperty(exports, "createLocale", {
4851
5449
  enumerable: true,
4852
5450
  get: function () { return yableCore.createLocale; }
4853
5451
  });
5452
+ Object.defineProperty(exports, "createUndoRedoIntegration", {
5453
+ enumerable: true,
5454
+ get: function () { return yableCore.createUndoRedoIntegration; }
5455
+ });
4854
5456
  Object.defineProperty(exports, "en", {
4855
5457
  enumerable: true,
4856
5458
  get: function () { return yableCore.en; }
@@ -4859,14 +5461,30 @@ Object.defineProperty(exports, "filterFns", {
4859
5461
  enumerable: true,
4860
5462
  get: function () { return yableCore.filterFns; }
4861
5463
  });
5464
+ Object.defineProperty(exports, "formulaFunctions", {
5465
+ enumerable: true,
5466
+ get: function () { return yableCore.formulaFunctions; }
5467
+ });
4862
5468
  Object.defineProperty(exports, "functionalUpdate", {
4863
5469
  enumerable: true,
4864
5470
  get: function () { return yableCore.functionalUpdate; }
4865
5471
  });
5472
+ Object.defineProperty(exports, "generatePivotColumnDefs", {
5473
+ enumerable: true,
5474
+ get: function () { return yableCore.generatePivotColumnDefs; }
5475
+ });
4866
5476
  Object.defineProperty(exports, "getDefaultLocale", {
4867
5477
  enumerable: true,
4868
5478
  get: function () { return yableCore.getDefaultLocale; }
4869
5479
  });
5480
+ Object.defineProperty(exports, "getInitialPivotState", {
5481
+ enumerable: true,
5482
+ get: function () { return yableCore.getInitialPivotState; }
5483
+ });
5484
+ Object.defineProperty(exports, "getPivotRowModel", {
5485
+ enumerable: true,
5486
+ get: function () { return yableCore.getPivotRowModel; }
5487
+ });
4870
5488
  Object.defineProperty(exports, "resetLocale", {
4871
5489
  enumerable: true,
4872
5490
  get: function () { return yableCore.resetLocale; }
@@ -4905,6 +5523,7 @@ exports.ExpandIcon = ExpandIcon;
4905
5523
  exports.FillHandle = FillHandle;
4906
5524
  exports.FiltersPanel = FiltersPanel;
4907
5525
  exports.FlashCell = FlashCell;
5526
+ exports.FloatingFilter = FloatingFilter;
4908
5527
  exports.GlobalFilter = GlobalFilter;
4909
5528
  exports.LoadingOverlay = LoadingOverlay;
4910
5529
  exports.MasterDetail = MasterDetail;
@@ -4912,6 +5531,7 @@ exports.NoRowsOverlay = NoRowsOverlay;
4912
5531
  exports.Pagination = Pagination;
4913
5532
  exports.PivotConfigPanel = PivotConfigPanel;
4914
5533
  exports.PrintLayout = PrintLayout;
5534
+ exports.SetFilter = SetFilter;
4915
5535
  exports.Sidebar = Sidebar;
4916
5536
  exports.SortIndicator = SortIndicator;
4917
5537
  exports.StatusBar = StatusBar;
@@ -4930,6 +5550,7 @@ exports.resolveMeasureRecipe = resolveMeasureRecipe;
4930
5550
  exports.useAutoMeasurements = useAutoMeasurements;
4931
5551
  exports.useCellFlash = useCellFlash;
4932
5552
  exports.useClipboard = useClipboard;
5553
+ exports.useColumnVirtualization = useColumnVirtualization;
4933
5554
  exports.useContextMenu = useContextMenu;
4934
5555
  exports.useFillHandle = useFillHandle;
4935
5556
  exports.useKeyboardNavigation = useKeyboardNavigation;