@lensui/lens-table 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -104,12 +104,12 @@ export function Example() {
104
104
  | `virtualized` | `boolean \| { enabled?: boolean; overscan?: number }` | `true` | 虚拟渲染配置。传`false` 时渲染全部行列;传对象时可通过 `enabled` 开关,并用 `overscan` 配置可见范围外的缓冲行列数量,默认 `4`。 |
105
105
  | `tooltip` | `boolean \| { header?: boolean; cell?: boolean }` | `true` | 文本 tooltip 总配置。传`true` 时表头和单元格 hover 都显示完整文本;传 `false` 可关闭;传对象可分别控制表头和单元格。 |
106
106
  | `summary` | `boolean \| { position?: 'top' \| 'bottom'; verticalBordered?: boolean; emptyValue?: ReactNode }` | `false` | 表格级合计行配置。默认不显示;传 `true` 或对象即开启合计行,对象可控制显示在表头下方或底部、是否显示内部竖线,以及无合计值单元格的填充内容。 |
107
- | `rowNumber` | `boolean` | `true` | 是否显示序号列。默认开启并自动生成左侧序号列,不需要在`columns` 中配置;传 `false` 可关闭。 |
107
+ | `rowNumber` | `boolean \| TableRowNumberConfig` | `true` | 是否显示序号列。默认开启并自动生成左侧序号列,不需要在`columns` 中配置;传 `false` 可关闭。传 `{ fixed: false }` 可取消序号列固定,`fixed` 默认 `true`。 |
108
108
  | `selectedCell` | `GridSelectionTarget \| null` | 非受控 | 受控单元格选中状态。传 `columnKey`,行可使用 `rowKey` 或 `rowIndex` 定位。 |
109
109
  | `defaultSelectedCell` | `GridSelectionTarget \| null` | `null` | 默认选中单元格。传 `columnKey`,行可使用 `rowKey` 或 `rowIndex` 定位。 |
110
110
  | `onSelectedCellChange` | `(selection) => void` | - | 单元格选中变化回调,`selection.value` 为当前单元格的原始值。 |
111
111
  | `rangeSelection` | `boolean` | `false` | 是否允许鼠标拖动选择多个单元格。开启后支持范围复制、范围右键菜单和右下角拖拽扩展范围。 |
112
- | `rowSelection` | `boolean \| AxisSelectionConfig` | `false` | 开启行选择。可传`{ mode: 'single' \| 'multiple' }`;开启后组件自动生成左侧选择列,不需要在 `columns` 中配置。 |
112
+ | `rowSelection` | `boolean \| RowSelectionConfig` | `false` | 开启行选择。可传`{ mode: 'single' \| 'multiple', showCheckbox: false }` 隐藏选择框并通过点击数据单元格选择行;默认显示选择框。默认自动生成左侧选择列,不需要在 `columns` 中配置。 |
113
113
  | `selectedRowKeys` | `GridKey[]` | 非受控 | 受控行选择 key 列表。 |
114
114
  | `defaultSelectedRowKeys` | `GridKey[]` | `[]` | 非受控模式下默认选中的行 key。 |
115
115
  | `onSelectedRowChange` | `(keys, rows, indices) => void` | - | 行选择变化回调。 |
@@ -128,6 +128,8 @@ export function Example() {
128
128
  | `onSortChange` | `(rows, state) => Row[]` | - | 自定义排序方法。排序状态由组件内部管理;返回排序后的行。 |
129
129
  | `onFilterChange` | `(rows, values) => Row[]` | - | 自定义筛选方法。筛选值由组件内部管理;返回符合条件的行。 |
130
130
  | `onCellChange` | `(change) => void` | - | 单元格编辑提交回调。 |
131
+ | `onCellClick` | `(cell: TableCellContext<Row>, event: React.MouseEvent<HTMLElement>) => void` | - | 数据单元格单击回调;调用 `event.preventDefault()` 可阻止默认选择行为。 |
132
+ | `onCellDoubleClick` | `(cell: TableCellContext<Row>, event: React.MouseEvent<HTMLElement>) => void` | - | 数据单元格双击回调,在编辑前触发;调用 `event.preventDefault()` 可阻止进入编辑。 |
131
133
  | `onCellContextMenu` | `(event) => void` | - | 单元格右键回调,可用于扩展自定义菜单逻辑。 |
132
134
  | `contextMenu` | `boolean \| { header?, cell?, range? }` | `true` | `true` 开启默认右键菜单,`false` 关闭右键菜单,传对象可自定义菜单项、顺序和分割线。 |
133
135
  | `emptyContent` | `ReactNode` | 内置空状态 | 自定义空数据内容。 |
@@ -206,7 +208,7 @@ export function Example() {
206
208
  | `width` | `number` | `140` | 列宽,单位 px。最小宽度由内部布局保护。 |
207
209
  | `fixed` | `'left' \| 'right'` | - | 固定列位置。左/右固定列会覆盖滚动列并显示阴影。 |
208
210
  | `align` | `'left' \| 'center' \| 'right'` | `'left'` | 单元格文本和拖拽预览文本对齐方式。 |
209
- | `editable` | `boolean` | `false` | 是否允许双击或按 Enter 进入编辑。 |
211
+ | `editable` | `boolean \| ((value: unknown, row: Row, rowIndex: number) => boolean)` | `false` | 是否允许双击、Enter 或右键菜单进入编辑。函数按当前单元格动态判断,提交时再次检查权限。 |
210
212
  | `editor` | `EditorConfig` | `{ type: 'text' }` | 内置编辑器配置。详见下方编辑器表。未配置时默认使用文本编辑器。 |
211
213
  | `sortable` | `boolean` | `true` | 表头显示排序按钮。普通数据列默认开启,传 `false` 可关闭。 |
212
214
  | `filterable` | `boolean` | `true` | 表头显示筛选按钮。普通数据列默认开启,传 `false` 可关闭。 |
@@ -494,7 +496,7 @@ const columns: GridColumn<Row>[] = [
494
496
 
495
497
  拖拽后组件会在内部维护新的行列顺序,因此排序回调不是必需的。需要持久化或同步结果时,可以通过回调直接取得排序后的 `rows` 或 `columns`。列宽调整默认开启并支持非受控使用,传 `columnResizable={false}` 可关闭。
496
498
 
497
- `columnDraggable` 默认开启,普通列会自动显示表头拖拽入口。行选择和行拖拽是表格级配置:开启 `rowSelection` 后自动生成左侧选择列,开启 `rowDraggable` 后自动生成左侧拖拽手柄列,不需要在 `columns` 里配置 `rowSelection` 或 `rowDragHandle`。
499
+ `columnDraggable` 默认开启,普通列会自动显示表头拖拽入口。行选择和行拖拽是表格级配置:开启 `rowSelection` 后默认自动生成左侧选择列(`showCheckbox: false` 时隐藏),开启 `rowDraggable` 后自动生成左侧拖拽手柄列,不需要在 `columns` 里配置 `rowSelection` 或 `rowDragHandle`。
498
500
 
499
501
  ```tsx
500
502
  <Table
@@ -514,6 +516,20 @@ const columns: GridColumn<Row>[] = [
514
516
  />;
515
517
  ```
516
518
 
519
+ 隐藏选择框时不生成选择列,表头全选框也会隐藏;行选择状态、回调和右键菜单仍然可用。多选模式支持 Ctrl / Command 点击追加或取消选择,以及 Shift 点击连续选择。
520
+
521
+ ```tsx
522
+ <Table
523
+ columns={columns}
524
+ rows={rows}
525
+ rowNumber={{ fixed: false }}
526
+ rowSelection={{ mode: 'multiple', showCheckbox: false }}
527
+ onSelectedRowChange={(keys, selectedRows) => console.log(keys, selectedRows)}
528
+ />
529
+ ```
530
+
531
+ `rowNumber={true}` 或 `rowNumber={{ fixed: true }}` 固定序号列;`rowNumber={false}` 隐藏序号列。
532
+
517
533
  ## 虚拟渲染
518
534
 
519
535
  默认开启虚拟渲染,只绘制可见区域和少量缓冲区域。缓冲数量默认是 `4`,可以通过对象形式调整。
@@ -693,3 +709,76 @@ pnpm check
693
709
  ## License
694
710
 
695
711
  MIT
712
+
713
+ ## 单元格事件与动态编辑权限
714
+
715
+ ```tsx
716
+ const columns: TableColumn<MyRow>[] = [
717
+ {
718
+ key: 'name',
719
+ title: '姓名',
720
+ dataIndex: 'name',
721
+ editable: (_value, row, _rowIndex) => row.status === 'draft',
722
+ },
723
+ ];
724
+
725
+ <Table
726
+ columns={columns}
727
+ rows={rows}
728
+ onCellClick={(cell, event) => {
729
+ console.log(cell.row, cell.column, cell.value, cell.rowIndex, cell.columnIndex);
730
+ }}
731
+ onCellDoubleClick={(cell, event) => {
732
+ console.log(cell.rowKey, cell.columnKey);
733
+ // event.preventDefault(); // 阻止本次双击进入编辑
734
+ }}
735
+ />
736
+ ```
737
+
738
+ 事件只针对数据单元格,表头、序号列、选择列和拖拽手柄不触发;只读单元格也会触发。双击时先触发一次单击回调,再触发双击回调,不延迟单击。合并单元格返回其起始单元格,索引基于当前显示顺序,`columnIndex` 包含工具列。
739
+
740
+ `editable` 保持兼容布尔值;函数接收当前值、行数据和当前行索引,返回 `true` 才允许编辑。编辑期间权限变为 `false` 时,提交会关闭编辑器并丢弃草稿,不触发 `onCellChange`。
741
+
742
+ ## Ref 方法
743
+
744
+ 支持 React 18+ 的对象 ref 和回调 ref,保留行数据的泛型推导;`VelocityGrid` 别名同样支持。可导入 `TableRef<Row>`(或 `VelocityGridRef<Row>`)。
745
+
746
+ ```tsx
747
+ import { useRef } from 'react';
748
+ import { Table, type TableRef } from '@lensui/lens-table';
749
+
750
+ function Example() {
751
+ const tableRef = useRef<TableRef<MyRow>>(null);
752
+ return (
753
+ <>
754
+ <button onClick={() => tableRef.current?.scrollToCell({ rowKey: 1, columnKey: 'name' })}>
755
+ 定位
756
+ </button>
757
+ <button onClick={() => tableRef.current?.startEdit({ rowKey: 1, columnKey: 'name' })}>
758
+ 编辑
759
+ </button>
760
+ <Table ref={tableRef} columns={columns} rows={rows} rowSelection />
761
+ </>
762
+ );
763
+ }
764
+ ```
765
+
766
+ | 方法 | 说明 |
767
+ | --- | --- |
768
+ | `focus()` | 聚焦表格,启用键盘操作。 |
769
+ | `scrollToCell(target)` | 将目标数据单元格滚入可见区域,不改变选择;目标不存在时返回 `false`。 |
770
+ | `getSelectedCell()` | 返回当前单元格的 `TableCellContext<Row>`,未选择时返回 `null`。 |
771
+ | `setSelectedCell(target \| null)` | 设置或清除单元格选择,同时清除范围选择;无效目标返回 `false` 并保留原选择。 |
772
+ | `getSelectedRowKeys()` | 返回所选行 key 的数组副本。 |
773
+ | `getSelectedRows()` | 返回当前筛选和排序结果中选中的行数据,按显示顺序排列。 |
774
+ | `setSelectedRowKeys(keys)` | 设置行选择并触发现有回调;需要开启 `rowSelection`,单选模式只取第一个 key。 |
775
+ | `getSelectedColumnKeys()` | 返回所选列 key 的数组副本。 |
776
+ | `setSelectedColumnKeys(keys)` | 设置数据列选择,忽略无效列;需要开启 `columnSelection`,单选模式只取第一个有效 key。 |
777
+ | `clearSelection()` | 清空单元格、范围、行和列选择,并取消编辑。 |
778
+ | `startEdit(target?)` | 选择并编辑目标单元格;省略参数时编辑当前选择。目标无效或 `editable` 不允许时返回 `false`。 |
779
+ | `commitEdit()` | 提交当前草稿并关闭编辑器,遵循 `editable` 动态权限和 `onCellChange`。 |
780
+ | `cancelEdit()` | 丢弃当前草稿并关闭编辑器。 |
781
+
782
+ `target` 使用 `{ rowKey, columnKey }` 或 `{ rowIndex, columnKey }`;`rowKey` 优先,`rowIndex` 为当前显示顺序中的零基索引。工具列不能作为目标;合并单元格自动定位到起始单元格。
783
+
784
+ 选择方法遵守受控/非受控约定:传入 `selectedCell`、`selectedRowKeys` 或 `selectedColumnKeys` 后,需要在相应回调中更新这些 props。尤其是受控单元格选择下的 `startEdit`,外部需要接受新的选择才能保持编辑器打开。读取方法反映最近一次 React 渲染完成后的状态,不保证在调用设置方法的同一同步代码段内立即更新;返回的行和列对象是原数据引用。
package/dist/index.cjs CHANGED
@@ -1522,7 +1522,7 @@ function resolveContextMenuSection(section, defaults) {
1522
1522
  }
1523
1523
  return defaults;
1524
1524
  }
1525
- function Table({
1525
+ function TableInner({
1526
1526
  columns: columnProps,
1527
1527
  rows: sourceRows,
1528
1528
  rowKey,
@@ -1568,13 +1568,15 @@ function Table({
1568
1568
  onSortChange,
1569
1569
  onFilterChange,
1570
1570
  onCellChange,
1571
+ onCellClick,
1572
+ onCellDoubleClick,
1571
1573
  onCellContextMenu,
1572
1574
  contextMenu: contextMenuConfig = true,
1573
1575
  emptyContent,
1574
1576
  className = "",
1575
1577
  style,
1576
1578
  ariaLabel = "Data grid"
1577
- }) {
1579
+ }, ref) {
1578
1580
  const [sourceColumns, setSourceColumns] = (0, import_react5.useState)(columnProps);
1579
1581
  (0, import_react5.useEffect)(() => {
1580
1582
  setSourceColumns((current) => reconcileColumns(current, columnProps));
@@ -1620,6 +1622,8 @@ function Table({
1620
1622
  const customContextMenuConfig = typeof contextMenuConfig === "object" ? contextMenuConfig : void 0;
1621
1623
  const [resizedColumnWidths, setResizedColumnWidths] = (0, import_react5.useState)({});
1622
1624
  const leafSourceColumns = (0, import_react5.useMemo)(() => flattenDataColumns(sourceColumns), [sourceColumns]);
1625
+ const sourceColumnWidthsRef = (0, import_react5.useRef)(new Map(leafSourceColumns.map((column) => [column.key, column.width])));
1626
+ const showRowCheckbox = Boolean(rowSelection) && (typeof rowSelection !== "object" || rowSelection.showCheckbox !== false);
1623
1627
  (0, import_react5.useEffect)(() => {
1624
1628
  if (!isProductionRuntime() && hasNestedFixedColumns(sourceColumns)) {
1625
1629
  console.warn("[Table] Multi-level headers only support fixed columns on top-level columns. Nested fixed values are ignored.");
@@ -1630,19 +1634,19 @@ function Table({
1630
1634
  if (rowDraggable) {
1631
1635
  utilityColumns.push({ key: "__rvg_row_drag__", title: "", width: 36, align: "center", fixed: "left", rowDragHandle: true });
1632
1636
  }
1633
- if (rowSelection) {
1637
+ if (showRowCheckbox) {
1634
1638
  utilityColumns.push({ key: "__rvg_row_selection__", title: "", width: 44, align: "center", fixed: "left", rowSelection: true });
1635
1639
  }
1636
1640
  if (rowNumber) {
1637
- utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1641
+ utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: typeof rowNumber === "object" && rowNumber.fixed === false ? void 0 : "left", rowNumber: true });
1638
1642
  }
1639
1643
  const dataColumns = leafSourceColumns.map((column) => {
1640
1644
  const resizedWidth = resizedColumnWidths[column.key];
1641
1645
  return resizedWidth === void 0 ? column : { ...column, width: resizedWidth };
1642
1646
  });
1643
1647
  return [...utilityColumns, ...dataColumns];
1644
- }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, rowSelection, sourceRows.length]);
1645
- const utilityColumnCount = (rowDraggable ? 1 : 0) + (rowSelection ? 1 : 0) + (rowNumber ? 1 : 0);
1648
+ }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, showRowCheckbox, sourceRows.length]);
1649
+ const utilityColumnCount = (rowDraggable ? 1 : 0) + (showRowCheckbox ? 1 : 0) + (rowNumber ? 1 : 0);
1646
1650
  const headerCells = (0, import_react5.useMemo)(() => buildHeaderCells(sourceColumns, utilityColumnCount, headerDepth), [headerDepth, sourceColumns, utilityColumnCount]);
1647
1651
  const activeCustomHeaderLevels = (0, import_react5.useMemo)(() => {
1648
1652
  const levels = /* @__PURE__ */ new Map();
@@ -1731,6 +1735,9 @@ function Table({
1731
1735
  setSelectionRange(null);
1732
1736
  }, [rangeSelection]);
1733
1737
  (0, import_react5.useEffect)(() => {
1738
+ const previousSourceWidths = sourceColumnWidthsRef.current;
1739
+ const nextSourceWidths = new Map(leafSourceColumns.map((column) => [column.key, column.width]));
1740
+ sourceColumnWidthsRef.current = nextSourceWidths;
1734
1741
  setResizedColumnWidths((current) => {
1735
1742
  const sourceColumnByKey = new Map(leafSourceColumns.map((column) => [column.key, column]));
1736
1743
  let changed = false;
@@ -1741,7 +1748,8 @@ function Table({
1741
1748
  changed = true;
1742
1749
  return;
1743
1750
  }
1744
- if (sourceColumn.width !== void 0 && sourceColumn.width !== value) {
1751
+ const sourceWidthChanged = previousSourceWidths.get(key) !== sourceColumn.width;
1752
+ if (sourceWidthChanged && sourceColumn.width !== void 0 && sourceColumn.width !== value) {
1745
1753
  changed = true;
1746
1754
  return;
1747
1755
  }
@@ -1978,7 +1986,11 @@ function Table({
1978
1986
  if (selection?.rowKey === editing.rowKey && selection.columnKey === editing.columnKey) return;
1979
1987
  setEditing(null);
1980
1988
  }, [editing, selection]);
1981
- const metrics = (0, import_react5.useMemo)(() => buildColumnMetrics(columns, { columnDraggable, viewportWidth: viewport.width }), [columnDraggable, columns, viewport.width]);
1989
+ const hasManualColumnWidths = Object.keys(resizedColumnWidths).length > 0;
1990
+ const metrics = (0, import_react5.useMemo)(() => buildColumnMetrics(columns, {
1991
+ columnDraggable,
1992
+ viewportWidth: hasManualColumnWidths ? 0 : viewport.width
1993
+ }), [columnDraggable, columns, hasManualColumnWidths, viewport.width]);
1982
1994
  const contentWidth = metrics.length > 0 ? metrics[metrics.length - 1].right : 0;
1983
1995
  const contentHeight = rows.length * rowHeight;
1984
1996
  const fixedWidth = (0, import_react5.useMemo)(() => columns.reduce((width2, column, index) => column.fixed === "left" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
@@ -2482,7 +2494,8 @@ function Table({
2482
2494
  const horizontalThumb = horizontalScrollbarThumbRef.current;
2483
2495
  if (!scroller || !verticalTrack || !verticalThumb || !horizontalTrack || !horizontalThumb) return;
2484
2496
  const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
2485
- if (maxScrollTop <= 1 || renderHeight <= 0) {
2497
+ const contentFitsAutoHeight = autoHeight && effectiveHeight >= realContentHeight - 1;
2498
+ if (contentFitsAutoHeight || maxScrollTop <= 1 || renderHeight <= 0) {
2486
2499
  verticalTrack.style.display = "none";
2487
2500
  } else {
2488
2501
  const top = fixedHeader ? bodyTop : 0;
@@ -2522,7 +2535,7 @@ function Table({
2522
2535
  horizontalTrack.style.bottom = `${bottomSummaryHeight + 2}px`;
2523
2536
  horizontalThumb.style.width = `${Math.min(trackWidth, thumbWidth)}px`;
2524
2537
  horizontalThumb.style.transform = `translateX(${thumbLeft}px)`;
2525
- }, [bodyTop, bottomSummaryHeight, effectiveHeight, fixedHeader, fixedWidth, renderHeight, rightFixedWidth, viewport.width]);
2538
+ }, [autoHeight, bodyTop, bottomSummaryHeight, effectiveHeight, fixedHeader, fixedWidth, realContentHeight, renderHeight, rightFixedWidth, viewport.width]);
2526
2539
  (0, import_react5.useLayoutEffect)(() => {
2527
2540
  updateCustomScrollbars();
2528
2541
  }, [contentHeight, contentWidth, updateCustomScrollbars, viewport.height, viewport.width]);
@@ -3348,9 +3361,15 @@ function Table({
3348
3361
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
3349
3362
  scheduleDraw();
3350
3363
  }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
3364
+ const isCellEditable = (0, import_react5.useCallback)((cell) => {
3365
+ const column = columns[cell.columnIndex];
3366
+ const row = rows[cell.rowIndex];
3367
+ if (!column || !row || column.dataIndex === void 0 || column.rowNumber || column.rowSelection || column.rowDragHandle) return false;
3368
+ return typeof column.editable === "function" ? column.editable(row[column.dataIndex], row, cell.rowIndex) : column.editable === true;
3369
+ }, [columns, rows]);
3351
3370
  const beginEdit = (0, import_react5.useCallback)((cell) => {
3352
3371
  const column = columns[cell.columnIndex];
3353
- if (!column?.editable || column.dataIndex === void 0) return;
3372
+ if (!isCellEditable(cell) || column.dataIndex === void 0) return;
3354
3373
  const value = rows[cell.rowIndex]?.[column.dataIndex];
3355
3374
  setDraft(value == null ? "" : String(value));
3356
3375
  const scroller = scrollerRef.current;
@@ -3392,7 +3411,7 @@ function Table({
3392
3411
  return;
3393
3412
  }
3394
3413
  setEditing(cell);
3395
- }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3414
+ }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, isCellEditable, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3396
3415
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
3397
3416
  const markCellEdited = (cell, previousValue, nextValue) => {
3398
3417
  const key = getCellEditKey(cell);
@@ -3420,6 +3439,10 @@ function Table({
3420
3439
  };
3421
3440
  const commitEdit = (0, import_react5.useCallback)((nextDraft, close = true) => {
3422
3441
  if (!editing) return;
3442
+ if (!isCellEditable(editing)) {
3443
+ setEditing(null);
3444
+ return;
3445
+ }
3423
3446
  const committedDraft = nextDraft ?? draft;
3424
3447
  const column = columns[editing.columnIndex];
3425
3448
  const row = rows[editing.rowIndex];
@@ -3434,7 +3457,7 @@ function Table({
3434
3457
  onCellChange?.({ ...editing, row, value: committedValue, previousValue });
3435
3458
  }
3436
3459
  if (close) setEditing(null);
3437
- }, [columns, draft, editing, onCellChange, rows]);
3460
+ }, [columns, draft, editing, isCellEditable, onCellChange, rows]);
3438
3461
  const renderEditor = () => {
3439
3462
  if (!editing || !editorStyle) return null;
3440
3463
  const column = columns[editing.columnIndex];
@@ -3565,6 +3588,18 @@ function Table({
3565
3588
  const { cell: resizeCell, edge: resizeEdge } = resizeHit;
3566
3589
  event.preventDefault();
3567
3590
  event.currentTarget.setPointerCapture(event.pointerId);
3591
+ setResizedColumnWidths((current) => {
3592
+ let changed = false;
3593
+ const next = { ...current };
3594
+ columns.forEach((column, index) => {
3595
+ if (column.rowSelection || column.rowDragHandle || column.rowNumber) return;
3596
+ const displayedWidth = metrics[index]?.width;
3597
+ if (displayedWidth === void 0 || next[column.key] === displayedWidth) return;
3598
+ next[column.key] = displayedWidth;
3599
+ changed = true;
3600
+ });
3601
+ return changed ? next : current;
3602
+ });
3568
3603
  const resizeIndex = resizeCell.endIndex;
3569
3604
  const resizeIndices = resizeCell.leaf ? [resizeIndex] : Array.from({ length: resizeCell.endIndex - resizeCell.startIndex + 1 }, (_, index) => resizeCell.startIndex + index);
3570
3605
  const startWidths = resizeIndices.map((index) => metrics[index].width);
@@ -3597,7 +3632,7 @@ function Table({
3597
3632
  const setGuideFromWidths = (nextWidths) => {
3598
3633
  const widthByIndex = new Map(drag.resizeIndices.map((columnIndex, index) => [columnIndex, nextWidths[index]]));
3599
3634
  const nextColumns = columns.map((column2, index) => widthByIndex.has(index) ? { ...column2, width: widthByIndex.get(index) } : column2);
3600
- const nextMetrics = buildColumnMetrics(nextColumns, { columnDraggable, viewportWidth: viewport.width });
3635
+ const nextMetrics = buildColumnMetrics(nextColumns, { columnDraggable, viewportWidth: 0 });
3601
3636
  setResizeGuideX(getHeaderCellEdgeFromMetrics(drag.resizeStartIndex, drag.resizeEndIndex, drag.resizeEdge, nextMetrics));
3602
3637
  };
3603
3638
  if (drag.resizeIndices.length > 1) {
@@ -4336,6 +4371,77 @@ function Table({
4336
4371
  `summary:${column.key}`
4337
4372
  );
4338
4373
  };
4374
+ (0, import_react5.useImperativeHandle)(ref, () => {
4375
+ const resolveDataCell = (target) => {
4376
+ const cell = resolveSelectionTarget(target);
4377
+ if (!cell) return null;
4378
+ const column = columns[cell.columnIndex];
4379
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return null;
4380
+ const span = getCellSpan(cell.rowIndex, cell.columnIndex);
4381
+ if (!span) return cell;
4382
+ return {
4383
+ rowIndex: span.rowIndex,
4384
+ columnIndex: span.columnIndex,
4385
+ rowKey: getRowKey(rows[span.rowIndex], span.rowIndex),
4386
+ columnKey: columns[span.columnIndex].key
4387
+ };
4388
+ };
4389
+ return {
4390
+ focus: () => scrollerRef.current?.focus({ preventScroll: true }),
4391
+ scrollToCell: (target) => {
4392
+ const cell = resolveDataCell(target);
4393
+ if (!cell) return false;
4394
+ revealCell(cell);
4395
+ return true;
4396
+ },
4397
+ getSelectedCell: () => {
4398
+ if (!selection) return null;
4399
+ const cell = resolveDataCell(selection);
4400
+ if (!cell) return null;
4401
+ const column = columns[cell.columnIndex];
4402
+ const row = rows[cell.rowIndex];
4403
+ return { ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] };
4404
+ },
4405
+ setSelectedCell: (target) => {
4406
+ const cell = target === null ? null : resolveDataCell(target);
4407
+ if (target !== null && !cell) return false;
4408
+ setSelectionRange(null);
4409
+ setSelection(cell);
4410
+ return true;
4411
+ },
4412
+ getSelectedRowKeys: () => [...rowKeys],
4413
+ getSelectedRows: () => rows.filter((row, index) => selectedRowKeySet.has(getRowKey(row, index))),
4414
+ setSelectedRowKeys: (keys) => {
4415
+ if (!rowSelection) return;
4416
+ const next = [...new Set(keys)];
4417
+ setRowKeys(rowSelectionMode === "single" ? next.slice(0, 1) : next);
4418
+ },
4419
+ getSelectedColumnKeys: () => [...columnKeys],
4420
+ setSelectedColumnKeys: (keys) => {
4421
+ if (!columnSelection) return;
4422
+ const next = [...new Set(keys)].filter((key) => columns.some((column) => column.key === key && !column.rowNumber && !column.rowSelection && !column.rowDragHandle));
4423
+ setColumnKeys(typeof columnSelection === "object" && columnSelection.mode === "single" ? next.slice(0, 1) : next);
4424
+ },
4425
+ clearSelection: () => {
4426
+ setSelectionRange(null);
4427
+ setSelection(null);
4428
+ setRowKeys([]);
4429
+ setColumnKeys([]);
4430
+ rowAnchorRef.current = null;
4431
+ setEditing(null);
4432
+ },
4433
+ startEdit: (target) => {
4434
+ const cell = target ? resolveDataCell(target) : selection ? resolveDataCell(selection) : null;
4435
+ if (!cell || !isCellEditable(cell)) return false;
4436
+ setSelectionRange(null);
4437
+ setSelection(cell);
4438
+ beginEdit(cell);
4439
+ return true;
4440
+ },
4441
+ commitEdit: () => commitEdit(),
4442
+ cancelEdit: () => setEditing(null)
4443
+ };
4444
+ });
4339
4445
  const rootStyle = {
4340
4446
  width,
4341
4447
  height: autoHeight ? effectiveHeight : height,
@@ -4534,6 +4640,10 @@ function Table({
4534
4640
  }
4535
4641
  if (event.detail > 1) return;
4536
4642
  clickedCellRef.current = cell;
4643
+ const column = columns[cell.columnIndex];
4644
+ const row = rows[cell.rowIndex];
4645
+ onCellClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4646
+ if (event.defaultPrevented) return;
4537
4647
  setSelectionRange(null);
4538
4648
  scrollerRef.current?.focus({ preventScroll: true });
4539
4649
  setSelection(cell);
@@ -4544,7 +4654,12 @@ function Table({
4544
4654
  },
4545
4655
  onDoubleClick: (event) => {
4546
4656
  const cell = clickedCellRef.current ?? locateCell(event.clientX, event.clientY);
4547
- if (cell) beginEdit(cell);
4657
+ if (!cell) return;
4658
+ const column = columns[cell.columnIndex];
4659
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return;
4660
+ const row = rows[cell.rowIndex];
4661
+ onCellDoubleClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4662
+ if (!event.defaultPrevented) beginEdit(cell);
4548
4663
  },
4549
4664
  onContextMenu: handleContextMenu
4550
4665
  }
@@ -4557,7 +4672,7 @@ function Table({
4557
4672
  className: "rvg-text-scroll-content",
4558
4673
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
4559
4674
  children: [
4560
- columns.map((column, columnIndex) => column.fixed || columnIndex < utilityColumnCount || headerDepth > 1 ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderTitle(columnIndex, getScrollableColumnLeft(columnIndex)) }, column.key) : null),
4675
+ columns.map((column, columnIndex) => column.fixed || columnIndex >= utilityColumnCount && headerDepth > 1 ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderTitle(columnIndex, getScrollableColumnLeft(columnIndex)) }, column.key) : null),
4561
4676
  headerCells.map((cell) => {
4562
4677
  if (columns[cell.startIndex]?.fixed) return null;
4563
4678
  if (cell.endIndex < domRange.columnStart || cell.startIndex >= domRange.columnEnd) return null;
@@ -4575,7 +4690,7 @@ function Table({
4575
4690
  const left = column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
4576
4691
  return renderCellText(rowIndex, columnIndex, left);
4577
4692
  })) }),
4578
- columns.map((column, columnIndex) => columnIndex < utilityColumnCount || headerDepth === 1 && (column.fixed === "left" || column.fixed === "right") ? renderHeaderTitle(columnIndex, column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width : column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : getScrollableColumnLeft(columnIndex)) : null),
4693
+ columns.map((column, columnIndex) => column.fixed && (columnIndex < utilityColumnCount || headerDepth === 1) ? renderHeaderTitle(columnIndex, column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width : column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : getScrollableColumnLeft(columnIndex)) : null),
4579
4694
  headerCells.map((cell) => columns[cell.startIndex]?.fixed ? renderGroupedHeaderCell(cell) : null),
4580
4695
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-shadow is-left", style: { left: fixedWidth } }),
4581
4696
  rightFixedWidth > 0 && currentScroll.left < contentWidth - viewport.width && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-shadow is-right", style: { right: rightFixedWidth } }),
@@ -4792,7 +4907,7 @@ function Table({
4792
4907
  const affectedRows = selectedIndices.includes(cell.rowIndex) ? selectedIndices : [cell.rowIndex];
4793
4908
  const cellContext = { ...cell, row, column, value: dataIndex === void 0 ? void 0 : row[dataIndex] };
4794
4909
  return renderConfiguredContextMenu(resolveContextMenuSection(customContextMenuConfig?.cell, DEFAULT_CELL_CONTEXT_MENU), cellContext, (item, override) => {
4795
- if (item === "edit") return renderContextMenuButton("cell-edit", cellContext, labels.edit, () => beginEdit(cell), override, { disabled: !column.editable, icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ContextMenuIcon, { type: "edit" }) });
4910
+ if (item === "edit") return renderContextMenuButton("cell-edit", cellContext, labels.edit, () => beginEdit(cell), override, { disabled: !isCellEditable(cell), icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ContextMenuIcon, { type: "edit" }) });
4796
4911
  if (item === "copy") return renderContextMenuButton("cell-copy", cellContext, labels.copyContent, async () => {
4797
4912
  try {
4798
4913
  await navigator.clipboard.writeText(getCellLabel(cell.rowIndex, cell.columnIndex));
@@ -5032,6 +5147,7 @@ function Table({
5032
5147
  loading && !hasCustomLoading && hasCompletedLoadRef.current && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-refresh-loading", "aria-label": labels.refreshing, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LoadingSpinnerIcon, {}) })
5033
5148
  ] });
5034
5149
  }
5150
+ var Table = (0, import_react5.forwardRef)(TableInner);
5035
5151
  // Annotate the CommonJS export names for ESM import in node:
5036
5152
  0 && (module.exports = {
5037
5153
  Table,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,4 @@
1
- import * as react from 'react';
2
- import { ReactNode, CSSProperties } from 'react';
1
+ import { ReactNode, MouseEvent, CSSProperties, RefAttributes, ReactElement } from 'react';
3
2
 
4
3
  type GridKey = string | number;
5
4
  type SelectionMode = 'single' | 'multiple';
@@ -155,6 +154,14 @@ interface AxisSelectionConfig {
155
154
  /** single uses radio-style selection, multiple uses checkbox-style selection. */
156
155
  mode?: SelectionMode;
157
156
  }
157
+ interface RowSelectionConfig extends AxisSelectionConfig {
158
+ /** Show the selection column. Defaults to true; when false, click body cells to select rows. */
159
+ showCheckbox?: boolean;
160
+ }
161
+ interface TableRowNumberConfig {
162
+ /** Fix the row number column to the left. Defaults to true. */
163
+ fixed?: boolean;
164
+ }
158
165
  /**
159
166
  * Column definition consumed by both the canvas renderer and DOM editor layer.
160
167
  *
@@ -179,7 +186,7 @@ interface GridColumn<Row> {
179
186
  /** Text alignment for body cells and drag previews. */
180
187
  align?: 'left' | 'center' | 'right';
181
188
  /** Enables double-click / Enter editing when an editor or fallback input exists. */
182
- editable?: boolean;
189
+ editable?: boolean | ((value: unknown, row: Row, rowIndex: number) => boolean);
183
190
  /** Built-in editor configuration for text, selectable, date, time, and range cells. */
184
191
  editor?: {
185
192
  type: 'text';
@@ -241,6 +248,31 @@ interface CellChange<Row> extends GridSelection {
241
248
  previousValue: unknown;
242
249
  row: Row;
243
250
  }
251
+ /** Data cell identity and content passed to mouse callbacks. */
252
+ interface TableCellContext<Row> extends GridSelection {
253
+ row: Row;
254
+ column: GridColumn<Row>;
255
+ value: unknown;
256
+ }
257
+ /** Imperative Table API. Selection getters reflect the latest committed render. */
258
+ interface TableRef<Row extends object> {
259
+ focus: () => void;
260
+ /** Reveal a data cell without changing selection; false if the target is unavailable. */
261
+ scrollToCell: (target: GridSelectionTarget) => boolean;
262
+ getSelectedCell: () => TableCellContext<Row> | null;
263
+ setSelectedCell: (target: GridSelectionTarget | null) => boolean;
264
+ getSelectedRowKeys: () => GridKey[];
265
+ getSelectedRows: () => Row[];
266
+ setSelectedRowKeys: (keys: GridKey[]) => void;
267
+ getSelectedColumnKeys: () => string[];
268
+ setSelectedColumnKeys: (keys: string[]) => void;
269
+ /** Clear cell, range, row, and column selections. */
270
+ clearSelection: () => void;
271
+ /** Select and edit a cell, or edit the current selection. Returns false for invalid or read-only cells. */
272
+ startEdit: (target?: GridSelectionTarget) => boolean;
273
+ commitEdit: () => void;
274
+ cancelEdit: () => void;
275
+ }
244
276
  /** Fired when the consumer wants to observe or extend a cell context menu. */
245
277
  interface ContextMenuEvent<Row> extends GridSelection {
246
278
  row: Row;
@@ -331,13 +363,13 @@ interface TableProps<Row extends object> {
331
363
  /** Show and position the summary row for columns with summary configured. */
332
364
  summary?: TableSummaryConfig;
333
365
  /** Show an automatically generated row number column on the left. */
334
- rowNumber?: boolean;
366
+ rowNumber?: boolean | TableRowNumberConfig;
335
367
  selectedCell?: GridSelectionTarget | null;
336
368
  defaultSelectedCell?: GridSelectionTarget | null;
337
369
  onSelectedCellChange?: (selection: SelectedCellChange | null) => void;
338
370
  /** Enable spreadsheet-style mouse drag selection across multiple body cells. Defaults to false. */
339
371
  rangeSelection?: boolean;
340
- rowSelection?: boolean | AxisSelectionConfig;
372
+ rowSelection?: boolean | RowSelectionConfig;
341
373
  selectedRowKeys?: GridKey[];
342
374
  defaultSelectedRowKeys?: GridKey[];
343
375
  onSelectedRowChange?: (keys: GridKey[], rows: Row[], indices: number[]) => void;
@@ -371,6 +403,10 @@ interface TableProps<Row extends object> {
371
403
  /** Overrides the built-in text filtering. Return the rows that match the internally managed filter values. */
372
404
  onFilterChange?: (rows: Row[], values: Readonly<Record<string, string>>) => Row[];
373
405
  onCellChange?: (change: CellChange<Row>) => void | Promise<void>;
406
+ /** Fires on a data cell click, before the default selection behavior. */
407
+ onCellClick?: (cell: TableCellContext<Row>, event: MouseEvent<HTMLElement>) => void;
408
+ /** Fires before double-click editing. preventDefault() cancels editing. */
409
+ onCellDoubleClick?: (cell: TableCellContext<Row>, event: MouseEvent<HTMLElement>) => void;
374
410
  onCellContextMenu?: (event: ContextMenuEvent<Row>) => void;
375
411
  /** true uses the built-in menu, false disables custom menus, object customizes menu content and order. */
376
412
  contextMenu?: TableContextMenu<Row>;
@@ -386,7 +422,7 @@ interface ViewportRange {
386
422
  columnEnd: number;
387
423
  }
388
424
 
389
- declare function Table<Row extends object>({ columns: columnProps, rows: sourceRows, rowKey, width, height, autoHeight, layout, rowHeight: rowHeightProp, headerHeight: headerHeightProp, fixedHeader, locale, borderless, verticalBorderless, striped, highlight, cellSpans, loading, loadingContent, virtualized, tooltip, summary, rowNumber, selectedCell, defaultSelectedCell, onSelectedCellChange, rangeSelection, rowSelection, selectedRowKeys, defaultSelectedRowKeys, onSelectedRowChange, columnSelection, selectedColumnKeys, defaultSelectedColumnKeys, onSelectedColumnChange, columnDraggable, columnResizable, onColumnsReorder, onColumnResize, rowDraggable, onRowsReorder, onInsertRows, onDeleteRows, onSortChange, onFilterChange, onCellChange, onCellContextMenu, contextMenu: contextMenuConfig, emptyContent, className, style, ariaLabel, }: TableProps<Row>): react.JSX.Element;
425
+ declare const Table: <Row extends object>(props: TableProps<Row> & RefAttributes<TableRef<Row>>) => ReactElement;
390
426
 
391
427
  type LayoutColumn<Row> = GridColumn<Row> & {
392
428
  rowSelection?: boolean;
@@ -433,4 +469,4 @@ declare function hitTestColumn(metrics: ColumnMetric[], contentX: number): numbe
433
469
 
434
470
  type TableFilterValues = Record<string, string>;
435
471
 
436
- export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSelectionTarget, type GridSortState, type SelectedCellChange, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type GridCellPosition as TableCellPosition, type TableCellSpan, type TableCellSpanRule, type TableCellSpanRuleContext, type TableCellSpans, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type GridKey as TableKey, type TableLabels, type TableLayoutConfig, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableResolvedCellSpan, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableCellSpanRule as VelocityGridCellSpanRule, type TableCellSpanRuleContext as VelocityGridCellSpanRuleContext, type TableCellSpans as VelocityGridCellSpans, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLayoutConfig as VelocityGridLayoutConfig, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
472
+ export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSelectionTarget, type GridSortState, type RowSelectionConfig, type SelectedCellChange, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type TableCellContext, type GridCellPosition as TableCellPosition, type TableCellSpan, type TableCellSpanRule, type TableCellSpanRuleContext, type TableCellSpans, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type GridKey as TableKey, type TableLabels, type TableLayoutConfig, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableRef, type TableResolvedCellSpan, type TableRowNumberConfig, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableCellSpanRule as VelocityGridCellSpanRule, type TableCellSpanRuleContext as VelocityGridCellSpanRuleContext, type TableCellSpans as VelocityGridCellSpans, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLayoutConfig as VelocityGridLayoutConfig, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableRef as VelocityGridRef, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import * as react from 'react';
2
- import { ReactNode, CSSProperties } from 'react';
1
+ import { ReactNode, MouseEvent, CSSProperties, RefAttributes, ReactElement } from 'react';
3
2
 
4
3
  type GridKey = string | number;
5
4
  type SelectionMode = 'single' | 'multiple';
@@ -155,6 +154,14 @@ interface AxisSelectionConfig {
155
154
  /** single uses radio-style selection, multiple uses checkbox-style selection. */
156
155
  mode?: SelectionMode;
157
156
  }
157
+ interface RowSelectionConfig extends AxisSelectionConfig {
158
+ /** Show the selection column. Defaults to true; when false, click body cells to select rows. */
159
+ showCheckbox?: boolean;
160
+ }
161
+ interface TableRowNumberConfig {
162
+ /** Fix the row number column to the left. Defaults to true. */
163
+ fixed?: boolean;
164
+ }
158
165
  /**
159
166
  * Column definition consumed by both the canvas renderer and DOM editor layer.
160
167
  *
@@ -179,7 +186,7 @@ interface GridColumn<Row> {
179
186
  /** Text alignment for body cells and drag previews. */
180
187
  align?: 'left' | 'center' | 'right';
181
188
  /** Enables double-click / Enter editing when an editor or fallback input exists. */
182
- editable?: boolean;
189
+ editable?: boolean | ((value: unknown, row: Row, rowIndex: number) => boolean);
183
190
  /** Built-in editor configuration for text, selectable, date, time, and range cells. */
184
191
  editor?: {
185
192
  type: 'text';
@@ -241,6 +248,31 @@ interface CellChange<Row> extends GridSelection {
241
248
  previousValue: unknown;
242
249
  row: Row;
243
250
  }
251
+ /** Data cell identity and content passed to mouse callbacks. */
252
+ interface TableCellContext<Row> extends GridSelection {
253
+ row: Row;
254
+ column: GridColumn<Row>;
255
+ value: unknown;
256
+ }
257
+ /** Imperative Table API. Selection getters reflect the latest committed render. */
258
+ interface TableRef<Row extends object> {
259
+ focus: () => void;
260
+ /** Reveal a data cell without changing selection; false if the target is unavailable. */
261
+ scrollToCell: (target: GridSelectionTarget) => boolean;
262
+ getSelectedCell: () => TableCellContext<Row> | null;
263
+ setSelectedCell: (target: GridSelectionTarget | null) => boolean;
264
+ getSelectedRowKeys: () => GridKey[];
265
+ getSelectedRows: () => Row[];
266
+ setSelectedRowKeys: (keys: GridKey[]) => void;
267
+ getSelectedColumnKeys: () => string[];
268
+ setSelectedColumnKeys: (keys: string[]) => void;
269
+ /** Clear cell, range, row, and column selections. */
270
+ clearSelection: () => void;
271
+ /** Select and edit a cell, or edit the current selection. Returns false for invalid or read-only cells. */
272
+ startEdit: (target?: GridSelectionTarget) => boolean;
273
+ commitEdit: () => void;
274
+ cancelEdit: () => void;
275
+ }
244
276
  /** Fired when the consumer wants to observe or extend a cell context menu. */
245
277
  interface ContextMenuEvent<Row> extends GridSelection {
246
278
  row: Row;
@@ -331,13 +363,13 @@ interface TableProps<Row extends object> {
331
363
  /** Show and position the summary row for columns with summary configured. */
332
364
  summary?: TableSummaryConfig;
333
365
  /** Show an automatically generated row number column on the left. */
334
- rowNumber?: boolean;
366
+ rowNumber?: boolean | TableRowNumberConfig;
335
367
  selectedCell?: GridSelectionTarget | null;
336
368
  defaultSelectedCell?: GridSelectionTarget | null;
337
369
  onSelectedCellChange?: (selection: SelectedCellChange | null) => void;
338
370
  /** Enable spreadsheet-style mouse drag selection across multiple body cells. Defaults to false. */
339
371
  rangeSelection?: boolean;
340
- rowSelection?: boolean | AxisSelectionConfig;
372
+ rowSelection?: boolean | RowSelectionConfig;
341
373
  selectedRowKeys?: GridKey[];
342
374
  defaultSelectedRowKeys?: GridKey[];
343
375
  onSelectedRowChange?: (keys: GridKey[], rows: Row[], indices: number[]) => void;
@@ -371,6 +403,10 @@ interface TableProps<Row extends object> {
371
403
  /** Overrides the built-in text filtering. Return the rows that match the internally managed filter values. */
372
404
  onFilterChange?: (rows: Row[], values: Readonly<Record<string, string>>) => Row[];
373
405
  onCellChange?: (change: CellChange<Row>) => void | Promise<void>;
406
+ /** Fires on a data cell click, before the default selection behavior. */
407
+ onCellClick?: (cell: TableCellContext<Row>, event: MouseEvent<HTMLElement>) => void;
408
+ /** Fires before double-click editing. preventDefault() cancels editing. */
409
+ onCellDoubleClick?: (cell: TableCellContext<Row>, event: MouseEvent<HTMLElement>) => void;
374
410
  onCellContextMenu?: (event: ContextMenuEvent<Row>) => void;
375
411
  /** true uses the built-in menu, false disables custom menus, object customizes menu content and order. */
376
412
  contextMenu?: TableContextMenu<Row>;
@@ -386,7 +422,7 @@ interface ViewportRange {
386
422
  columnEnd: number;
387
423
  }
388
424
 
389
- declare function Table<Row extends object>({ columns: columnProps, rows: sourceRows, rowKey, width, height, autoHeight, layout, rowHeight: rowHeightProp, headerHeight: headerHeightProp, fixedHeader, locale, borderless, verticalBorderless, striped, highlight, cellSpans, loading, loadingContent, virtualized, tooltip, summary, rowNumber, selectedCell, defaultSelectedCell, onSelectedCellChange, rangeSelection, rowSelection, selectedRowKeys, defaultSelectedRowKeys, onSelectedRowChange, columnSelection, selectedColumnKeys, defaultSelectedColumnKeys, onSelectedColumnChange, columnDraggable, columnResizable, onColumnsReorder, onColumnResize, rowDraggable, onRowsReorder, onInsertRows, onDeleteRows, onSortChange, onFilterChange, onCellChange, onCellContextMenu, contextMenu: contextMenuConfig, emptyContent, className, style, ariaLabel, }: TableProps<Row>): react.JSX.Element;
425
+ declare const Table: <Row extends object>(props: TableProps<Row> & RefAttributes<TableRef<Row>>) => ReactElement;
390
426
 
391
427
  type LayoutColumn<Row> = GridColumn<Row> & {
392
428
  rowSelection?: boolean;
@@ -433,4 +469,4 @@ declare function hitTestColumn(metrics: ColumnMetric[], contentX: number): numbe
433
469
 
434
470
  type TableFilterValues = Record<string, string>;
435
471
 
436
- export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSelectionTarget, type GridSortState, type SelectedCellChange, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type GridCellPosition as TableCellPosition, type TableCellSpan, type TableCellSpanRule, type TableCellSpanRuleContext, type TableCellSpans, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type GridKey as TableKey, type TableLabels, type TableLayoutConfig, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableResolvedCellSpan, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableCellSpanRule as VelocityGridCellSpanRule, type TableCellSpanRuleContext as VelocityGridCellSpanRuleContext, type TableCellSpans as VelocityGridCellSpans, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLayoutConfig as VelocityGridLayoutConfig, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
472
+ export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSelectionTarget, type GridSortState, type RowSelectionConfig, type SelectedCellChange, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type TableCellContext, type GridCellPosition as TableCellPosition, type TableCellSpan, type TableCellSpanRule, type TableCellSpanRuleContext, type TableCellSpans, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type GridKey as TableKey, type TableLabels, type TableLayoutConfig, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableRef, type TableResolvedCellSpan, type TableRowNumberConfig, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableCellSpanRule as VelocityGridCellSpanRule, type TableCellSpanRuleContext as VelocityGridCellSpanRuleContext, type TableCellSpans as VelocityGridCellSpans, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLayoutConfig as VelocityGridLayoutConfig, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableRef as VelocityGridRef, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/Table.tsx
2
- import { Fragment as Fragment2, useCallback as useCallback2, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useMemo, useRef as useRef2, useState as useState4 } from "react";
2
+ import { forwardRef, useImperativeHandle, Fragment as Fragment2, useCallback as useCallback2, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useMemo, useRef as useRef2, useState as useState4 } from "react";
3
3
  import { flushSync } from "react-dom";
4
4
 
5
5
  // src/components/EmptyState.tsx
@@ -1492,7 +1492,7 @@ function resolveContextMenuSection(section, defaults) {
1492
1492
  }
1493
1493
  return defaults;
1494
1494
  }
1495
- function Table({
1495
+ function TableInner({
1496
1496
  columns: columnProps,
1497
1497
  rows: sourceRows,
1498
1498
  rowKey,
@@ -1538,13 +1538,15 @@ function Table({
1538
1538
  onSortChange,
1539
1539
  onFilterChange,
1540
1540
  onCellChange,
1541
+ onCellClick,
1542
+ onCellDoubleClick,
1541
1543
  onCellContextMenu,
1542
1544
  contextMenu: contextMenuConfig = true,
1543
1545
  emptyContent,
1544
1546
  className = "",
1545
1547
  style,
1546
1548
  ariaLabel = "Data grid"
1547
- }) {
1549
+ }, ref) {
1548
1550
  const [sourceColumns, setSourceColumns] = useState4(columnProps);
1549
1551
  useEffect2(() => {
1550
1552
  setSourceColumns((current) => reconcileColumns(current, columnProps));
@@ -1590,6 +1592,8 @@ function Table({
1590
1592
  const customContextMenuConfig = typeof contextMenuConfig === "object" ? contextMenuConfig : void 0;
1591
1593
  const [resizedColumnWidths, setResizedColumnWidths] = useState4({});
1592
1594
  const leafSourceColumns = useMemo(() => flattenDataColumns(sourceColumns), [sourceColumns]);
1595
+ const sourceColumnWidthsRef = useRef2(new Map(leafSourceColumns.map((column) => [column.key, column.width])));
1596
+ const showRowCheckbox = Boolean(rowSelection) && (typeof rowSelection !== "object" || rowSelection.showCheckbox !== false);
1593
1597
  useEffect2(() => {
1594
1598
  if (!isProductionRuntime() && hasNestedFixedColumns(sourceColumns)) {
1595
1599
  console.warn("[Table] Multi-level headers only support fixed columns on top-level columns. Nested fixed values are ignored.");
@@ -1600,19 +1604,19 @@ function Table({
1600
1604
  if (rowDraggable) {
1601
1605
  utilityColumns.push({ key: "__rvg_row_drag__", title: "", width: 36, align: "center", fixed: "left", rowDragHandle: true });
1602
1606
  }
1603
- if (rowSelection) {
1607
+ if (showRowCheckbox) {
1604
1608
  utilityColumns.push({ key: "__rvg_row_selection__", title: "", width: 44, align: "center", fixed: "left", rowSelection: true });
1605
1609
  }
1606
1610
  if (rowNumber) {
1607
- utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1611
+ utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: typeof rowNumber === "object" && rowNumber.fixed === false ? void 0 : "left", rowNumber: true });
1608
1612
  }
1609
1613
  const dataColumns = leafSourceColumns.map((column) => {
1610
1614
  const resizedWidth = resizedColumnWidths[column.key];
1611
1615
  return resizedWidth === void 0 ? column : { ...column, width: resizedWidth };
1612
1616
  });
1613
1617
  return [...utilityColumns, ...dataColumns];
1614
- }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, rowSelection, sourceRows.length]);
1615
- const utilityColumnCount = (rowDraggable ? 1 : 0) + (rowSelection ? 1 : 0) + (rowNumber ? 1 : 0);
1618
+ }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, showRowCheckbox, sourceRows.length]);
1619
+ const utilityColumnCount = (rowDraggable ? 1 : 0) + (showRowCheckbox ? 1 : 0) + (rowNumber ? 1 : 0);
1616
1620
  const headerCells = useMemo(() => buildHeaderCells(sourceColumns, utilityColumnCount, headerDepth), [headerDepth, sourceColumns, utilityColumnCount]);
1617
1621
  const activeCustomHeaderLevels = useMemo(() => {
1618
1622
  const levels = /* @__PURE__ */ new Map();
@@ -1701,6 +1705,9 @@ function Table({
1701
1705
  setSelectionRange(null);
1702
1706
  }, [rangeSelection]);
1703
1707
  useEffect2(() => {
1708
+ const previousSourceWidths = sourceColumnWidthsRef.current;
1709
+ const nextSourceWidths = new Map(leafSourceColumns.map((column) => [column.key, column.width]));
1710
+ sourceColumnWidthsRef.current = nextSourceWidths;
1704
1711
  setResizedColumnWidths((current) => {
1705
1712
  const sourceColumnByKey = new Map(leafSourceColumns.map((column) => [column.key, column]));
1706
1713
  let changed = false;
@@ -1711,7 +1718,8 @@ function Table({
1711
1718
  changed = true;
1712
1719
  return;
1713
1720
  }
1714
- if (sourceColumn.width !== void 0 && sourceColumn.width !== value) {
1721
+ const sourceWidthChanged = previousSourceWidths.get(key) !== sourceColumn.width;
1722
+ if (sourceWidthChanged && sourceColumn.width !== void 0 && sourceColumn.width !== value) {
1715
1723
  changed = true;
1716
1724
  return;
1717
1725
  }
@@ -1948,7 +1956,11 @@ function Table({
1948
1956
  if (selection?.rowKey === editing.rowKey && selection.columnKey === editing.columnKey) return;
1949
1957
  setEditing(null);
1950
1958
  }, [editing, selection]);
1951
- const metrics = useMemo(() => buildColumnMetrics(columns, { columnDraggable, viewportWidth: viewport.width }), [columnDraggable, columns, viewport.width]);
1959
+ const hasManualColumnWidths = Object.keys(resizedColumnWidths).length > 0;
1960
+ const metrics = useMemo(() => buildColumnMetrics(columns, {
1961
+ columnDraggable,
1962
+ viewportWidth: hasManualColumnWidths ? 0 : viewport.width
1963
+ }), [columnDraggable, columns, hasManualColumnWidths, viewport.width]);
1952
1964
  const contentWidth = metrics.length > 0 ? metrics[metrics.length - 1].right : 0;
1953
1965
  const contentHeight = rows.length * rowHeight;
1954
1966
  const fixedWidth = useMemo(() => columns.reduce((width2, column, index) => column.fixed === "left" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
@@ -2452,7 +2464,8 @@ function Table({
2452
2464
  const horizontalThumb = horizontalScrollbarThumbRef.current;
2453
2465
  if (!scroller || !verticalTrack || !verticalThumb || !horizontalTrack || !horizontalThumb) return;
2454
2466
  const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
2455
- if (maxScrollTop <= 1 || renderHeight <= 0) {
2467
+ const contentFitsAutoHeight = autoHeight && effectiveHeight >= realContentHeight - 1;
2468
+ if (contentFitsAutoHeight || maxScrollTop <= 1 || renderHeight <= 0) {
2456
2469
  verticalTrack.style.display = "none";
2457
2470
  } else {
2458
2471
  const top = fixedHeader ? bodyTop : 0;
@@ -2492,7 +2505,7 @@ function Table({
2492
2505
  horizontalTrack.style.bottom = `${bottomSummaryHeight + 2}px`;
2493
2506
  horizontalThumb.style.width = `${Math.min(trackWidth, thumbWidth)}px`;
2494
2507
  horizontalThumb.style.transform = `translateX(${thumbLeft}px)`;
2495
- }, [bodyTop, bottomSummaryHeight, effectiveHeight, fixedHeader, fixedWidth, renderHeight, rightFixedWidth, viewport.width]);
2508
+ }, [autoHeight, bodyTop, bottomSummaryHeight, effectiveHeight, fixedHeader, fixedWidth, realContentHeight, renderHeight, rightFixedWidth, viewport.width]);
2496
2509
  useLayoutEffect2(() => {
2497
2510
  updateCustomScrollbars();
2498
2511
  }, [contentHeight, contentWidth, updateCustomScrollbars, viewport.height, viewport.width]);
@@ -3318,9 +3331,15 @@ function Table({
3318
3331
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
3319
3332
  scheduleDraw();
3320
3333
  }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
3334
+ const isCellEditable = useCallback2((cell) => {
3335
+ const column = columns[cell.columnIndex];
3336
+ const row = rows[cell.rowIndex];
3337
+ if (!column || !row || column.dataIndex === void 0 || column.rowNumber || column.rowSelection || column.rowDragHandle) return false;
3338
+ return typeof column.editable === "function" ? column.editable(row[column.dataIndex], row, cell.rowIndex) : column.editable === true;
3339
+ }, [columns, rows]);
3321
3340
  const beginEdit = useCallback2((cell) => {
3322
3341
  const column = columns[cell.columnIndex];
3323
- if (!column?.editable || column.dataIndex === void 0) return;
3342
+ if (!isCellEditable(cell) || column.dataIndex === void 0) return;
3324
3343
  const value = rows[cell.rowIndex]?.[column.dataIndex];
3325
3344
  setDraft(value == null ? "" : String(value));
3326
3345
  const scroller = scrollerRef.current;
@@ -3362,7 +3381,7 @@ function Table({
3362
3381
  return;
3363
3382
  }
3364
3383
  setEditing(cell);
3365
- }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3384
+ }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, isCellEditable, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3366
3385
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
3367
3386
  const markCellEdited = (cell, previousValue, nextValue) => {
3368
3387
  const key = getCellEditKey(cell);
@@ -3390,6 +3409,10 @@ function Table({
3390
3409
  };
3391
3410
  const commitEdit = useCallback2((nextDraft, close = true) => {
3392
3411
  if (!editing) return;
3412
+ if (!isCellEditable(editing)) {
3413
+ setEditing(null);
3414
+ return;
3415
+ }
3393
3416
  const committedDraft = nextDraft ?? draft;
3394
3417
  const column = columns[editing.columnIndex];
3395
3418
  const row = rows[editing.rowIndex];
@@ -3404,7 +3427,7 @@ function Table({
3404
3427
  onCellChange?.({ ...editing, row, value: committedValue, previousValue });
3405
3428
  }
3406
3429
  if (close) setEditing(null);
3407
- }, [columns, draft, editing, onCellChange, rows]);
3430
+ }, [columns, draft, editing, isCellEditable, onCellChange, rows]);
3408
3431
  const renderEditor = () => {
3409
3432
  if (!editing || !editorStyle) return null;
3410
3433
  const column = columns[editing.columnIndex];
@@ -3535,6 +3558,18 @@ function Table({
3535
3558
  const { cell: resizeCell, edge: resizeEdge } = resizeHit;
3536
3559
  event.preventDefault();
3537
3560
  event.currentTarget.setPointerCapture(event.pointerId);
3561
+ setResizedColumnWidths((current) => {
3562
+ let changed = false;
3563
+ const next = { ...current };
3564
+ columns.forEach((column, index) => {
3565
+ if (column.rowSelection || column.rowDragHandle || column.rowNumber) return;
3566
+ const displayedWidth = metrics[index]?.width;
3567
+ if (displayedWidth === void 0 || next[column.key] === displayedWidth) return;
3568
+ next[column.key] = displayedWidth;
3569
+ changed = true;
3570
+ });
3571
+ return changed ? next : current;
3572
+ });
3538
3573
  const resizeIndex = resizeCell.endIndex;
3539
3574
  const resizeIndices = resizeCell.leaf ? [resizeIndex] : Array.from({ length: resizeCell.endIndex - resizeCell.startIndex + 1 }, (_, index) => resizeCell.startIndex + index);
3540
3575
  const startWidths = resizeIndices.map((index) => metrics[index].width);
@@ -3567,7 +3602,7 @@ function Table({
3567
3602
  const setGuideFromWidths = (nextWidths) => {
3568
3603
  const widthByIndex = new Map(drag.resizeIndices.map((columnIndex, index) => [columnIndex, nextWidths[index]]));
3569
3604
  const nextColumns = columns.map((column2, index) => widthByIndex.has(index) ? { ...column2, width: widthByIndex.get(index) } : column2);
3570
- const nextMetrics = buildColumnMetrics(nextColumns, { columnDraggable, viewportWidth: viewport.width });
3605
+ const nextMetrics = buildColumnMetrics(nextColumns, { columnDraggable, viewportWidth: 0 });
3571
3606
  setResizeGuideX(getHeaderCellEdgeFromMetrics(drag.resizeStartIndex, drag.resizeEndIndex, drag.resizeEdge, nextMetrics));
3572
3607
  };
3573
3608
  if (drag.resizeIndices.length > 1) {
@@ -4306,6 +4341,77 @@ function Table({
4306
4341
  `summary:${column.key}`
4307
4342
  );
4308
4343
  };
4344
+ useImperativeHandle(ref, () => {
4345
+ const resolveDataCell = (target) => {
4346
+ const cell = resolveSelectionTarget(target);
4347
+ if (!cell) return null;
4348
+ const column = columns[cell.columnIndex];
4349
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return null;
4350
+ const span = getCellSpan(cell.rowIndex, cell.columnIndex);
4351
+ if (!span) return cell;
4352
+ return {
4353
+ rowIndex: span.rowIndex,
4354
+ columnIndex: span.columnIndex,
4355
+ rowKey: getRowKey(rows[span.rowIndex], span.rowIndex),
4356
+ columnKey: columns[span.columnIndex].key
4357
+ };
4358
+ };
4359
+ return {
4360
+ focus: () => scrollerRef.current?.focus({ preventScroll: true }),
4361
+ scrollToCell: (target) => {
4362
+ const cell = resolveDataCell(target);
4363
+ if (!cell) return false;
4364
+ revealCell(cell);
4365
+ return true;
4366
+ },
4367
+ getSelectedCell: () => {
4368
+ if (!selection) return null;
4369
+ const cell = resolveDataCell(selection);
4370
+ if (!cell) return null;
4371
+ const column = columns[cell.columnIndex];
4372
+ const row = rows[cell.rowIndex];
4373
+ return { ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] };
4374
+ },
4375
+ setSelectedCell: (target) => {
4376
+ const cell = target === null ? null : resolveDataCell(target);
4377
+ if (target !== null && !cell) return false;
4378
+ setSelectionRange(null);
4379
+ setSelection(cell);
4380
+ return true;
4381
+ },
4382
+ getSelectedRowKeys: () => [...rowKeys],
4383
+ getSelectedRows: () => rows.filter((row, index) => selectedRowKeySet.has(getRowKey(row, index))),
4384
+ setSelectedRowKeys: (keys) => {
4385
+ if (!rowSelection) return;
4386
+ const next = [...new Set(keys)];
4387
+ setRowKeys(rowSelectionMode === "single" ? next.slice(0, 1) : next);
4388
+ },
4389
+ getSelectedColumnKeys: () => [...columnKeys],
4390
+ setSelectedColumnKeys: (keys) => {
4391
+ if (!columnSelection) return;
4392
+ const next = [...new Set(keys)].filter((key) => columns.some((column) => column.key === key && !column.rowNumber && !column.rowSelection && !column.rowDragHandle));
4393
+ setColumnKeys(typeof columnSelection === "object" && columnSelection.mode === "single" ? next.slice(0, 1) : next);
4394
+ },
4395
+ clearSelection: () => {
4396
+ setSelectionRange(null);
4397
+ setSelection(null);
4398
+ setRowKeys([]);
4399
+ setColumnKeys([]);
4400
+ rowAnchorRef.current = null;
4401
+ setEditing(null);
4402
+ },
4403
+ startEdit: (target) => {
4404
+ const cell = target ? resolveDataCell(target) : selection ? resolveDataCell(selection) : null;
4405
+ if (!cell || !isCellEditable(cell)) return false;
4406
+ setSelectionRange(null);
4407
+ setSelection(cell);
4408
+ beginEdit(cell);
4409
+ return true;
4410
+ },
4411
+ commitEdit: () => commitEdit(),
4412
+ cancelEdit: () => setEditing(null)
4413
+ };
4414
+ });
4309
4415
  const rootStyle = {
4310
4416
  width,
4311
4417
  height: autoHeight ? effectiveHeight : height,
@@ -4504,6 +4610,10 @@ function Table({
4504
4610
  }
4505
4611
  if (event.detail > 1) return;
4506
4612
  clickedCellRef.current = cell;
4613
+ const column = columns[cell.columnIndex];
4614
+ const row = rows[cell.rowIndex];
4615
+ onCellClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4616
+ if (event.defaultPrevented) return;
4507
4617
  setSelectionRange(null);
4508
4618
  scrollerRef.current?.focus({ preventScroll: true });
4509
4619
  setSelection(cell);
@@ -4514,7 +4624,12 @@ function Table({
4514
4624
  },
4515
4625
  onDoubleClick: (event) => {
4516
4626
  const cell = clickedCellRef.current ?? locateCell(event.clientX, event.clientY);
4517
- if (cell) beginEdit(cell);
4627
+ if (!cell) return;
4628
+ const column = columns[cell.columnIndex];
4629
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return;
4630
+ const row = rows[cell.rowIndex];
4631
+ onCellDoubleClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4632
+ if (!event.defaultPrevented) beginEdit(cell);
4518
4633
  },
4519
4634
  onContextMenu: handleContextMenu
4520
4635
  }
@@ -4527,7 +4642,7 @@ function Table({
4527
4642
  className: "rvg-text-scroll-content",
4528
4643
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
4529
4644
  children: [
4530
- columns.map((column, columnIndex) => column.fixed || columnIndex < utilityColumnCount || headerDepth > 1 ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ jsx7("span", { children: renderHeaderTitle(columnIndex, getScrollableColumnLeft(columnIndex)) }, column.key) : null),
4645
+ columns.map((column, columnIndex) => column.fixed || columnIndex >= utilityColumnCount && headerDepth > 1 ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ jsx7("span", { children: renderHeaderTitle(columnIndex, getScrollableColumnLeft(columnIndex)) }, column.key) : null),
4531
4646
  headerCells.map((cell) => {
4532
4647
  if (columns[cell.startIndex]?.fixed) return null;
4533
4648
  if (cell.endIndex < domRange.columnStart || cell.startIndex >= domRange.columnEnd) return null;
@@ -4545,7 +4660,7 @@ function Table({
4545
4660
  const left = column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
4546
4661
  return renderCellText(rowIndex, columnIndex, left);
4547
4662
  })) }),
4548
- columns.map((column, columnIndex) => columnIndex < utilityColumnCount || headerDepth === 1 && (column.fixed === "left" || column.fixed === "right") ? renderHeaderTitle(columnIndex, column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width : column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : getScrollableColumnLeft(columnIndex)) : null),
4663
+ columns.map((column, columnIndex) => column.fixed && (columnIndex < utilityColumnCount || headerDepth === 1) ? renderHeaderTitle(columnIndex, column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width : column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : getScrollableColumnLeft(columnIndex)) : null),
4549
4664
  headerCells.map((cell) => columns[cell.startIndex]?.fixed ? renderGroupedHeaderCell(cell) : null),
4550
4665
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-shadow is-left", style: { left: fixedWidth } }),
4551
4666
  rightFixedWidth > 0 && currentScroll.left < contentWidth - viewport.width && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-shadow is-right", style: { right: rightFixedWidth } }),
@@ -4762,7 +4877,7 @@ function Table({
4762
4877
  const affectedRows = selectedIndices.includes(cell.rowIndex) ? selectedIndices : [cell.rowIndex];
4763
4878
  const cellContext = { ...cell, row, column, value: dataIndex === void 0 ? void 0 : row[dataIndex] };
4764
4879
  return renderConfiguredContextMenu(resolveContextMenuSection(customContextMenuConfig?.cell, DEFAULT_CELL_CONTEXT_MENU), cellContext, (item, override) => {
4765
- if (item === "edit") return renderContextMenuButton("cell-edit", cellContext, labels.edit, () => beginEdit(cell), override, { disabled: !column.editable, icon: /* @__PURE__ */ jsx7(ContextMenuIcon, { type: "edit" }) });
4880
+ if (item === "edit") return renderContextMenuButton("cell-edit", cellContext, labels.edit, () => beginEdit(cell), override, { disabled: !isCellEditable(cell), icon: /* @__PURE__ */ jsx7(ContextMenuIcon, { type: "edit" }) });
4766
4881
  if (item === "copy") return renderContextMenuButton("cell-copy", cellContext, labels.copyContent, async () => {
4767
4882
  try {
4768
4883
  await navigator.clipboard.writeText(getCellLabel(cell.rowIndex, cell.columnIndex));
@@ -5002,6 +5117,7 @@ function Table({
5002
5117
  loading && !hasCustomLoading && hasCompletedLoadRef.current && /* @__PURE__ */ jsx7("div", { className: "rvg-refresh-loading", "aria-label": labels.refreshing, children: /* @__PURE__ */ jsx7(LoadingSpinnerIcon, {}) })
5003
5118
  ] });
5004
5119
  }
5120
+ var Table = forwardRef(TableInner);
5005
5121
  export {
5006
5122
  Table,
5007
5123
  Table as VelocityGrid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensui/lens-table",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "A lightweight, canvas-rendered React table component for LensUI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",