twico-ui 1.25.0 → 1.26.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.d.cts CHANGED
@@ -1938,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1938
1938
  onStateChange?: (state: DatatableState) => void;
1939
1939
  /** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
1940
1940
  showPageJumper?: boolean;
1941
- /** Click-to-select mode: "row" highlights the clicked row, "cell" highlights a single cell. @default "none" */
1941
+ /** Click-to-select mode: "row" highlights the clicked row, "cell" enables spreadsheet-like cell selection.
1942
+ * In "cell" mode the grid supports keyboard navigation and **rectangular range selection** (#317):
1943
+ * Shift+Click / Shift+Arrow extend a range from an anchor; selected cells expose `aria-selected` and the
1944
+ * grid is `aria-multiselectable` with `aria-activedescendant` on the active cell. @default "none" */
1942
1945
  selectionMode?: "none" | "row" | "cell";
1943
1946
  /** Fired when a row is clicked in "row" selection mode: (row, key). */
1944
1947
  onRowClick?: (row: T, key: string | number) => void;
@@ -1946,6 +1949,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1946
1949
  onCellClick?: (value: any, row: T, field: string) => void;
1947
1950
  /** Fired when the active cell changes: ({ key, field } | null). */
1948
1951
  onActiveCellChange?: (cell: { key: string | number; field: string } | null) => void;
1952
+ /** #317: fired when the rectangular cell selection changes ("cell" mode) — every cell inside the
1953
+ * anchor→focus rectangle, in row-major order. Empty array when the selection clears. */
1954
+ onCellSelectionChange?: (cells: Array<{ key: string | number; field: string }>) => void;
1955
+ /** #318: enable spreadsheet clipboard on cells ("cell" mode): Ctrl/Cmd+C copies the active cell/range as
1956
+ * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
+ * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
+ * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
1959
+ * @default false */
1960
+ enableClipboard?: boolean;
1949
1961
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1950
1962
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1951
1963
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2157,6 +2169,11 @@ interface DatatableColumn<T = any> {
2157
2169
  loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
2158
2170
  /** Format the raw value to a string/number for display. */
2159
2171
  valueFormatter?: (value: any, row: T) => React.ReactNode;
2172
+ /** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
2173
+ * paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
2174
+ * "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
2175
+ * from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
2176
+ copyType?: string;
2160
2177
  /** Fully custom cell renderer (badges, avatars, etc.). */
2161
2178
  renderCell?: (value: any, row: T) => React.ReactNode;
2162
2179
  /** Diff-mode (#239) per-column equality override — return true when the two values are equal.
package/dist/index.d.ts CHANGED
@@ -1938,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1938
1938
  onStateChange?: (state: DatatableState) => void;
1939
1939
  /** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
1940
1940
  showPageJumper?: boolean;
1941
- /** Click-to-select mode: "row" highlights the clicked row, "cell" highlights a single cell. @default "none" */
1941
+ /** Click-to-select mode: "row" highlights the clicked row, "cell" enables spreadsheet-like cell selection.
1942
+ * In "cell" mode the grid supports keyboard navigation and **rectangular range selection** (#317):
1943
+ * Shift+Click / Shift+Arrow extend a range from an anchor; selected cells expose `aria-selected` and the
1944
+ * grid is `aria-multiselectable` with `aria-activedescendant` on the active cell. @default "none" */
1942
1945
  selectionMode?: "none" | "row" | "cell";
1943
1946
  /** Fired when a row is clicked in "row" selection mode: (row, key). */
1944
1947
  onRowClick?: (row: T, key: string | number) => void;
@@ -1946,6 +1949,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1946
1949
  onCellClick?: (value: any, row: T, field: string) => void;
1947
1950
  /** Fired when the active cell changes: ({ key, field } | null). */
1948
1951
  onActiveCellChange?: (cell: { key: string | number; field: string } | null) => void;
1952
+ /** #317: fired when the rectangular cell selection changes ("cell" mode) — every cell inside the
1953
+ * anchor→focus rectangle, in row-major order. Empty array when the selection clears. */
1954
+ onCellSelectionChange?: (cells: Array<{ key: string | number; field: string }>) => void;
1955
+ /** #318: enable spreadsheet clipboard on cells ("cell" mode): Ctrl/Cmd+C copies the active cell/range as
1956
+ * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
+ * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
+ * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
1959
+ * @default false */
1960
+ enableClipboard?: boolean;
1949
1961
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1950
1962
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1951
1963
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2157,6 +2169,11 @@ interface DatatableColumn<T = any> {
2157
2169
  loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
2158
2170
  /** Format the raw value to a string/number for display. */
2159
2171
  valueFormatter?: (value: any, row: T) => React.ReactNode;
2172
+ /** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
2173
+ * paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
2174
+ * "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
2175
+ * from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
2176
+ copyType?: string;
2160
2177
  /** Fully custom cell renderer (badges, avatars, etc.). */
2161
2178
  renderCell?: (value: any, row: T) => React.ReactNode;
2162
2179
  /** Diff-mode (#239) per-column equality override — return true when the two values are equal.
package/dist/index.mjs CHANGED
@@ -8440,6 +8440,8 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8440
8440
  .twc-dt__row[data-selectable="true"] { cursor: pointer; }
8441
8441
  .twc-dt__row[data-active="true"] .twc-dt__td { background: var(--color-primary-subtle); box-shadow: inset 0 0 0 9999px transparent; }
8442
8442
  .twc-dt__row[data-active="true"] .twc-dt__td[data-pin] { background: var(--color-primary-subtle); }
8443
+ /* #317: data-cell-selected fills the range rectangle; data-cell-active keeps the ring on the moving corner. */
8444
+ .twc-dt__td[data-cell-selected="true"] { background: var(--color-primary-subtle); }
8443
8445
  .twc-dt__td[data-cell-active="true"] { box-shadow: inset 0 0 0 2px var(--color-primary); background: var(--color-primary-subtle); }
8444
8446
  .twc-dt__row:last-child .twc-dt__td { border-bottom: none; }
8445
8447
 
@@ -8594,6 +8596,9 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8594
8596
  .twc-dt__pop-grip:hover, .twc-dt__pop-grip:focus-visible, .twc-dt__pop-grip[data-active="true"] { opacity: 1; }
8595
8597
  .twc-dt__pop-grip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
8596
8598
  [dir="rtl"] .twc-dt__pop-grip { cursor: nesw-resize; }
8599
+ /* #316: a second grip in the bottom-inline-start corner grows a right-anchored panel toward the empty side (right edge pinned). */
8600
+ .twc-dt__pop-grip[data-side="start"] { inset-inline-start: 2px; inset-inline-end: auto; cursor: nesw-resize; background: linear-gradient(225deg, transparent 0 55%, var(--color-border-strong) 55% 64%, transparent 64% 73%, var(--color-border-strong) 73% 82%, transparent 82%); }
8601
+ [dir="rtl"] .twc-dt__pop-grip[data-side="start"] { cursor: nwse-resize; }
8597
8602
  @media (pointer: coarse) {
8598
8603
  .twc-dt__f-rz { width: 16px; inset-inline-end: -6px; opacity: 0.4; }
8599
8604
  .twc-dt__pop-grip { width: 20px; height: 20px; opacity: 0.6; }
@@ -9148,6 +9153,8 @@ function Datatable({
9148
9153
  onRowClick,
9149
9154
  onCellClick,
9150
9155
  onActiveCellChange,
9156
+ onCellSelectionChange,
9157
+ enableClipboard = false,
9151
9158
  showAggregation = false,
9152
9159
  ariaLabel = "Data table",
9153
9160
  "aria-label": ariaLabelAttr,
@@ -9278,6 +9285,8 @@ function Datatable({
9278
9285
  const [selected, setSelected] = React51__default.useState(() => /* @__PURE__ */ new Set());
9279
9286
  const [activeRow, setActiveRow] = React51__default.useState(null);
9280
9287
  const [activeCell, setActiveCell] = React51__default.useState(null);
9288
+ const [anchorCell, setAnchorCell] = React51__default.useState(null);
9289
+ const gridId = React51__default.useId();
9281
9290
  const [internalPage, setInternalPage] = React51__default.useState(0);
9282
9291
  const [internalRpp, setInternalRpp] = React51__default.useState(pageSize > 0 ? pageSize : 10);
9283
9292
  const pageControlled = page !== void 0;
@@ -9377,6 +9386,7 @@ function Datatable({
9377
9386
  for (const [id, sz] of Object.entries(s.popoverSizes)) {
9378
9387
  if (!sz || typeof sz !== "object" || typeof sz.w !== "number" || typeof sz.h !== "number") continue;
9379
9388
  next[id] = { w: clampNum(sz.w, popMinW(id), fMaxW()), h: clampNum(sz.h, 120, fMaxH()) };
9389
+ if (typeof sz.left === "number") next[id].left = clampNum(sz.left, 8, fMaxW());
9380
9390
  }
9381
9391
  if (Object.keys(next).length) setPopSizes(next);
9382
9392
  }
@@ -9828,6 +9838,38 @@ function Datatable({
9828
9838
  middleRows.forEach((r2, i) => m.set(keyOf(r2, keyIndex.get(keyOf(r2)) ?? i), i));
9829
9839
  return m;
9830
9840
  }, [middleRows, keyIndex]);
9841
+ const cellColIndex = React51__default.useCallback((field) => ordered.findIndex((c) => c.field === field), [ordered]);
9842
+ const cellRect = React51__default.useMemo(() => {
9843
+ if (selectionMode !== "cell" || !activeCell) return null;
9844
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9845
+ if (ar == null || ac < 0) return null;
9846
+ const anchor = anchorCell || activeCell;
9847
+ const nr = keyIndex.get(anchor.key), nc = cellColIndex(anchor.field);
9848
+ const r0 = nr == null ? ar : Math.min(ar, nr), r1 = nr == null ? ar : Math.max(ar, nr);
9849
+ const c0 = nc < 0 ? ac : Math.min(ac, nc), c1 = nc < 0 ? ac : Math.max(ac, nc);
9850
+ return { r0, r1, c0, c1 };
9851
+ }, [selectionMode, activeCell, anchorCell, keyIndex, cellColIndex]);
9852
+ const activeCellId = React51__default.useMemo(() => {
9853
+ if (selectionMode !== "cell" || !activeCell) return void 0;
9854
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9855
+ return ar == null || ac < 0 ? void 0 : `${gridId}-${ar}-${ac}`;
9856
+ }, [selectionMode, activeCell, keyIndex, cellColIndex, gridId]);
9857
+ const onCellSelRef = React51__default.useRef(onCellSelectionChange);
9858
+ onCellSelRef.current = onCellSelectionChange;
9859
+ const prevSelSigRef = React51__default.useRef("");
9860
+ React51__default.useEffect(() => {
9861
+ const cb = onCellSelRef.current;
9862
+ if (selectionMode !== "cell" || !cb) return;
9863
+ const cells = [];
9864
+ if (cellRect) for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) for (let c = cellRect.c0; c <= cellRect.c1; c++) {
9865
+ const row = leafRows[r2], col = ordered[c];
9866
+ if (row && col) cells.push({ key: keyOf(row, r2), field: col.field });
9867
+ }
9868
+ const sig = cells.map((x) => `${x.key}|${x.field}`).join(",");
9869
+ if (sig === prevSelSigRef.current) return;
9870
+ prevSelSigRef.current = sig;
9871
+ cb(cells);
9872
+ }, [cellRect, selectionMode, leafRows, ordered]);
9831
9873
  const canReorderRows = rowReorder && !activeGroupBy.length && !sort;
9832
9874
  function onRowDrop(targetKey) {
9833
9875
  const fromKey = rowDrag.from;
@@ -10240,6 +10282,7 @@ function Datatable({
10240
10282
  const next = { key: k, field: col.field };
10241
10283
  setActiveCell(next);
10242
10284
  setActiveRow(k);
10285
+ if (!(e.shiftKey && anchorCell)) setAnchorCell(next);
10243
10286
  onCellClick?.(row[col.field], row, col.field);
10244
10287
  onActiveCellChange?.(next);
10245
10288
  }
@@ -10251,6 +10294,24 @@ function Datatable({
10251
10294
  if (editing) return;
10252
10295
  const td = e.target.closest(".twc-dt__td[data-r]");
10253
10296
  if (!td) return;
10297
+ if (enableClipboard && selectionMode === "cell" && (e.ctrlKey || e.metaKey)) {
10298
+ const k = e.key.toLowerCase();
10299
+ if (k === "c") {
10300
+ e.preventDefault();
10301
+ copySelection(false);
10302
+ return;
10303
+ }
10304
+ if (k === "x") {
10305
+ e.preventDefault();
10306
+ copySelection(true);
10307
+ return;
10308
+ }
10309
+ if (k === "v") {
10310
+ e.preventDefault();
10311
+ pasteSelection();
10312
+ return;
10313
+ }
10314
+ }
10254
10315
  let r2 = +td.getAttribute("data-r"), c = +td.getAttribute("data-c");
10255
10316
  const maxR = leafRows.length - 1, maxC = ordered.length - 1;
10256
10317
  let handled = true;
@@ -10303,6 +10364,16 @@ function Datatable({
10303
10364
  e.preventDefault();
10304
10365
  setFocus({ r: r2, c });
10305
10366
  focusCell(r2, c);
10367
+ if (selectionMode === "cell") {
10368
+ const nk = keyOf(leafRows[r2], r2), col = ordered[c];
10369
+ if (col) {
10370
+ const nc = { key: nk, field: col.field };
10371
+ setActiveCell(nc);
10372
+ setActiveRow(nk);
10373
+ if (!e.shiftKey) setAnchorCell(nc);
10374
+ onActiveCellChange?.(nc);
10375
+ }
10376
+ }
10306
10377
  }
10307
10378
  }
10308
10379
  React51__default.useEffect(() => {
@@ -10419,6 +10490,142 @@ function Datatable({
10419
10490
  setBatchEdit(null);
10420
10491
  closeBatchEdit();
10421
10492
  }
10493
+ const [clipboardMsg, setClipboardMsg] = React51__default.useState("");
10494
+ const clipboardRef = React51__default.useRef(null);
10495
+ const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10496
+ const writeClipboard = (text) => {
10497
+ const execFallback = () => {
10498
+ try {
10499
+ if (typeof document !== "undefined") {
10500
+ const ta = document.createElement("textarea");
10501
+ ta.value = text;
10502
+ ta.style.position = "fixed";
10503
+ ta.style.opacity = "0";
10504
+ document.body.appendChild(ta);
10505
+ ta.focus();
10506
+ ta.select();
10507
+ document.execCommand("copy");
10508
+ document.body.removeChild(ta);
10509
+ }
10510
+ } catch {
10511
+ }
10512
+ };
10513
+ try {
10514
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).catch(execFallback);
10515
+ } catch {
10516
+ }
10517
+ execFallback();
10518
+ return Promise.resolve();
10519
+ };
10520
+ const writeCellPatches = (patchByKey) => {
10521
+ if (!patchByKey.size) return 0;
10522
+ const changed = [];
10523
+ const nextAll = rows.map((r2, i) => {
10524
+ const k = keyOf(r2, i);
10525
+ const patch = patchByKey.get(k);
10526
+ if (!patch) return r2;
10527
+ const updated = { ...r2, ...patch };
10528
+ changed.push([updated, r2, patch]);
10529
+ return updated;
10530
+ });
10531
+ changed.forEach(([updated, orig, patch]) => {
10532
+ for (const f of Object.keys(patch)) onRowUpdate?.(updated, orig, f);
10533
+ });
10534
+ if (onRowsChange && changed.length) onRowsChange(nextAll);
10535
+ return changed.length;
10536
+ };
10537
+ const rectCells = () => {
10538
+ if (!cellRect) return null;
10539
+ const out = [];
10540
+ for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) {
10541
+ const row = leafRows[r2];
10542
+ if (!row) continue;
10543
+ const line = [];
10544
+ for (let c = cellRect.c0; c <= cellRect.c1; c++) {
10545
+ const col = ordered[c];
10546
+ if (col) line.push({ key: keyOf(row, r2), field: col.field, col, value: getColVal(col, row) });
10547
+ }
10548
+ out.push(line);
10549
+ }
10550
+ return out.length ? out : null;
10551
+ };
10552
+ const copySelection = (cut) => {
10553
+ const grid = rectCells();
10554
+ if (!grid) return;
10555
+ const tsv = grid.map((line) => line.map((cell) => cell.value == null ? "" : String(cell.value)).join(" ")).join("\n");
10556
+ clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10557
+ writeClipboard(tsv);
10558
+ const n = grid.reduce((a, line) => a + line.length, 0);
10559
+ if (cut) {
10560
+ const patchByKey = /* @__PURE__ */ new Map();
10561
+ let clearable = 0;
10562
+ for (const line of grid) for (const cell of line) {
10563
+ if (!isColEditable(cell.col)) continue;
10564
+ clearable++;
10565
+ const p = patchByKey.get(cell.key) || {};
10566
+ p[cell.field] = cell.col.type === "number" ? null : "";
10567
+ patchByKey.set(cell.key, p);
10568
+ }
10569
+ writeCellPatches(patchByKey);
10570
+ setClipboardMsg(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`);
10571
+ } else {
10572
+ setClipboardMsg(`Copied ${n} cell${n === 1 ? "" : "s"}`);
10573
+ }
10574
+ };
10575
+ const pasteSelection = async () => {
10576
+ if (!cellRect) return;
10577
+ let text = null;
10578
+ try {
10579
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
10580
+ } catch {
10581
+ setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
10582
+ return;
10583
+ }
10584
+ if (text == null) {
10585
+ setClipboardMsg("Nothing to paste");
10586
+ return;
10587
+ }
10588
+ const inApp = clipboardRef.current && clipboardRef.current.tsv === text ? clipboardRef.current : null;
10589
+ let matrix;
10590
+ if (inApp) matrix = inApp.matrix;
10591
+ else {
10592
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
10593
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
10594
+ matrix = lines.map((line) => line.split(" "));
10595
+ }
10596
+ const patchByKey = /* @__PURE__ */ new Map();
10597
+ let written = 0, skipped = 0;
10598
+ for (let i = 0; i < matrix.length; i++) {
10599
+ const rr = cellRect.r0 + i;
10600
+ const row = leafRows[rr];
10601
+ if (!row) break;
10602
+ for (let j = 0; j < matrix[i].length; j++) {
10603
+ const cc = cellRect.c0 + j;
10604
+ const col = ordered[cc];
10605
+ if (!col) break;
10606
+ if (!isColEditable(col)) {
10607
+ skipped++;
10608
+ continue;
10609
+ }
10610
+ if (inApp && inApp.colTypes[j] != null && inApp.colTypes[j] !== copyTypeOf(col)) {
10611
+ skipped++;
10612
+ continue;
10613
+ }
10614
+ let v = matrix[i][j];
10615
+ if (col.type === "number") {
10616
+ v = v === "" ? null : Number(v);
10617
+ if (Number.isNaN(v)) v = null;
10618
+ }
10619
+ const k = keyOf(row, rr);
10620
+ const p = patchByKey.get(k) || {};
10621
+ p[col.field] = v;
10622
+ patchByKey.set(k, p);
10623
+ written++;
10624
+ }
10625
+ }
10626
+ writeCellPatches(patchByKey);
10627
+ setClipboardMsg(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`);
10628
+ };
10422
10629
  React51__default.useEffect(() => {
10423
10630
  if (!editing) return;
10424
10631
  const onDown = (e) => {
@@ -10675,6 +10882,7 @@ function Datatable({
10675
10882
  if (node) {
10676
10883
  node.style.removeProperty("--twc-dt-pop-w");
10677
10884
  node.style.removeProperty("--twc-dt-pop-h");
10885
+ node.style.removeProperty("left");
10678
10886
  node.removeAttribute("data-pop-sized");
10679
10887
  }
10680
10888
  setPopSizes((s) => {
@@ -10684,7 +10892,7 @@ function Datatable({
10684
10892
  return n;
10685
10893
  });
10686
10894
  };
10687
- const startPopResize = (id) => (e) => {
10895
+ const startPopResize = (id, side) => (e) => {
10688
10896
  if (e.button != null && e.button !== 0) return;
10689
10897
  e.preventDefault();
10690
10898
  e.stopPropagation();
@@ -10693,21 +10901,27 @@ function Datatable({
10693
10901
  const grip = e.currentTarget;
10694
10902
  const startX = e.clientX, startY = e.clientY;
10695
10903
  const r2 = pop.getBoundingClientRect();
10696
- const startW = r2.width, startH = r2.height;
10904
+ const startW = r2.width, startH = r2.height, startLeft = r2.left, startRight = r2.right;
10697
10905
  const dir = typeof getComputedStyle !== "undefined" && getComputedStyle(pop).direction === "rtl" ? -1 : 1;
10906
+ const toStart = side === "start";
10698
10907
  pop.style.setProperty("--twc-dt-pop-w", `${startW}px`);
10699
10908
  pop.style.setProperty("--twc-dt-pop-h", `${startH}px`);
10700
10909
  grip.setAttribute("data-active", "true");
10701
10910
  pop.setAttribute("data-pop-sized", "");
10702
- let lw = startW, lh = startH;
10911
+ let lw = startW, lh = startH, ll = startLeft;
10703
10912
  const onMove = (ev) => {
10704
- lw = clampNum(startW + (ev.clientX - startX) * dir, popMinW(id), fMaxW());
10913
+ const dw = toStart ? (startX - ev.clientX) * dir : (ev.clientX - startX) * dir;
10914
+ lw = clampNum(startW + dw, popMinW(id), fMaxW());
10705
10915
  lh = clampNum(startH + (ev.clientY - startY), 120, fMaxH());
10706
10916
  pop.style.setProperty("--twc-dt-pop-w", `${lw}px`);
10707
10917
  pop.style.setProperty("--twc-dt-pop-h", `${lh}px`);
10918
+ if (toStart) {
10919
+ ll = Math.max(8, startRight - lw);
10920
+ pop.style.left = `${ll}px`;
10921
+ }
10708
10922
  };
10709
10923
  const onUp = () => {
10710
- setPopSizes((s) => ({ ...s, [id]: { w: lw, h: lh } }));
10924
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w: lw, h: lh, ...toStart ? { left: ll } : {} } }));
10711
10925
  grip.removeAttribute("data-active");
10712
10926
  window.removeEventListener("pointermove", onMove);
10713
10927
  window.removeEventListener("pointerup", onUp);
@@ -10715,7 +10929,7 @@ function Datatable({
10715
10929
  window.addEventListener("pointermove", onMove);
10716
10930
  window.addEventListener("pointerup", onUp);
10717
10931
  };
10718
- const onPopGripKey = (id) => (e) => {
10932
+ const onPopGripKey = (id, side) => (e) => {
10719
10933
  if (e.key === "Enter" || e.key === " " || e.key === "Backspace") {
10720
10934
  e.preventDefault();
10721
10935
  clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
@@ -10740,25 +10954,54 @@ function Datatable({
10740
10954
  w = fMaxW();
10741
10955
  h = fMaxH();
10742
10956
  }
10743
- setPopSizes((s) => ({ ...s, [id]: { w: clampNum(w, popMinW(id), fMaxW()), h: clampNum(h, 120, fMaxH()) } }));
10957
+ w = clampNum(w, popMinW(id), fMaxW());
10958
+ h = clampNum(h, 120, fMaxH());
10959
+ const left = side === "start" ? Math.max(8, r2.right - w) : void 0;
10960
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w, h, ...left != null ? { left } : {} } }));
10744
10961
  };
10745
- const popStyle = (id) => popSizes[id] ? { "--twc-dt-pop-w": `${popSizes[id].w}px`, "--twc-dt-pop-h": `${popSizes[id].h}px` } : null;
10746
- const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsx(
10747
- "span",
10748
- {
10749
- className: "twc-dt__pop-grip",
10750
- role: "slider",
10751
- tabIndex: 0,
10752
- "aria-label": "Resize panel (Enter or double-click to reset)",
10753
- "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10754
- onPointerDown: startPopResize(id),
10755
- onKeyDown: onPopGripKey(id),
10756
- onDoubleClick: (e) => {
10757
- e.preventDefault();
10758
- clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
10962
+ const popStyle = (id) => {
10963
+ const s = popSizes[id];
10964
+ if (!s) return null;
10965
+ const st = { "--twc-dt-pop-w": `${s.w}px`, "--twc-dt-pop-h": `${s.h}px` };
10966
+ if (s.left != null) st.left = `${s.left}px`;
10967
+ return st;
10968
+ };
10969
+ const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsxs(Fragment, { children: [
10970
+ /* @__PURE__ */ jsx(
10971
+ "span",
10972
+ {
10973
+ className: "twc-dt__pop-grip",
10974
+ "data-side": "end",
10975
+ role: "slider",
10976
+ tabIndex: 0,
10977
+ "aria-label": "Resize panel (Enter or double-click to reset)",
10978
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10979
+ onPointerDown: startPopResize(id, "end"),
10980
+ onKeyDown: onPopGripKey(id, "end"),
10981
+ onDoubleClick: (e) => {
10982
+ e.preventDefault();
10983
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
10984
+ }
10759
10985
  }
10760
- }
10761
- ) : null;
10986
+ ),
10987
+ /* @__PURE__ */ jsx(
10988
+ "span",
10989
+ {
10990
+ className: "twc-dt__pop-grip",
10991
+ "data-side": "start",
10992
+ role: "slider",
10993
+ tabIndex: 0,
10994
+ "aria-label": "Resize panel leftward (Enter or double-click to reset)",
10995
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10996
+ onPointerDown: startPopResize(id, "start"),
10997
+ onKeyDown: onPopGripKey(id, "start"),
10998
+ onDoubleClick: (e) => {
10999
+ e.preventDefault();
11000
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11001
+ }
11002
+ }
11003
+ )
11004
+ ] }) : null;
10762
11005
  const fieldHandle = (field, rowIdx) => resizableFilters && rowIdx === 0 ? /* @__PURE__ */ jsx(
10763
11006
  "span",
10764
11007
  {
@@ -10914,22 +11157,27 @@ function Datatable({
10914
11157
  const editable = isColEditable(c);
10915
11158
  const isEditing = editing && editing.key === k && editing.field === c.field;
10916
11159
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
11160
+ const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
11161
+ const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
10917
11162
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
10918
11163
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
10919
11164
  return /* @__PURE__ */ jsxs(
10920
11165
  "td",
10921
11166
  {
11167
+ id: cellId,
10922
11168
  className: "twc-dt__td",
10923
11169
  role: "gridcell",
10924
11170
  "data-r": ri,
10925
11171
  "data-c": ci,
10926
11172
  "aria-colindex": ci + 1 + (checkboxSelection ? 1 : 0),
10927
11173
  tabIndex: focus.r === ri && focus.c === ci ? 0 : -1,
11174
+ "aria-selected": cellSelected || void 0,
10928
11175
  "data-num": c.type === "number" || void 0,
10929
11176
  "data-actions": isActions || void 0,
10930
11177
  "data-editable": editable && !isEditing || void 0,
10931
11178
  "data-editing": isEditing || void 0,
10932
11179
  "data-cell-active": cellActive || void 0,
11180
+ "data-cell-selected": cellSelected || void 0,
10933
11181
  "data-wrap": wrapped.has(c.field) || void 0,
10934
11182
  "data-pin": st.pin,
10935
11183
  "data-pin-edge": st.edge,
@@ -11291,6 +11539,7 @@ function Datatable({
11291
11539
  /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__groupbar-clear", onClick: () => setGroupBy([]), children: "Clear all" })
11292
11540
  ] }) : null,
11293
11541
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
11542
+ /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
11294
11543
  pivotActive ? renderPivot() : /* @__PURE__ */ jsx(
11295
11544
  "div",
11296
11545
  {
@@ -11312,6 +11561,8 @@ function Datatable({
11312
11561
  "aria-label": ariaLabelAttr || ariaLabel,
11313
11562
  "aria-rowcount": totalRows + 1,
11314
11563
  "aria-colcount": ordered.length + (checkboxSelection ? 1 : 0),
11564
+ "aria-multiselectable": selectionMode === "cell" || void 0,
11565
+ "aria-activedescendant": activeCellId,
11315
11566
  "aria-busy": loading || void 0,
11316
11567
  onKeyDown: onGridKeyDown,
11317
11568
  children: [