@lensui/lens-table 0.1.6 → 0.1.7

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,7 @@ 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 showRowCheckbox = Boolean(rowSelection) && (typeof rowSelection !== "object" || rowSelection.showCheckbox !== false);
1623
1626
  (0, import_react5.useEffect)(() => {
1624
1627
  if (!isProductionRuntime() && hasNestedFixedColumns(sourceColumns)) {
1625
1628
  console.warn("[Table] Multi-level headers only support fixed columns on top-level columns. Nested fixed values are ignored.");
@@ -1630,19 +1633,19 @@ function Table({
1630
1633
  if (rowDraggable) {
1631
1634
  utilityColumns.push({ key: "__rvg_row_drag__", title: "", width: 36, align: "center", fixed: "left", rowDragHandle: true });
1632
1635
  }
1633
- if (rowSelection) {
1636
+ if (showRowCheckbox) {
1634
1637
  utilityColumns.push({ key: "__rvg_row_selection__", title: "", width: 44, align: "center", fixed: "left", rowSelection: true });
1635
1638
  }
1636
1639
  if (rowNumber) {
1637
- utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1640
+ 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
1641
  }
1639
1642
  const dataColumns = leafSourceColumns.map((column) => {
1640
1643
  const resizedWidth = resizedColumnWidths[column.key];
1641
1644
  return resizedWidth === void 0 ? column : { ...column, width: resizedWidth };
1642
1645
  });
1643
1646
  return [...utilityColumns, ...dataColumns];
1644
- }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, rowSelection, sourceRows.length]);
1645
- const utilityColumnCount = (rowDraggable ? 1 : 0) + (rowSelection ? 1 : 0) + (rowNumber ? 1 : 0);
1647
+ }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, showRowCheckbox, sourceRows.length]);
1648
+ const utilityColumnCount = (rowDraggable ? 1 : 0) + (showRowCheckbox ? 1 : 0) + (rowNumber ? 1 : 0);
1646
1649
  const headerCells = (0, import_react5.useMemo)(() => buildHeaderCells(sourceColumns, utilityColumnCount, headerDepth), [headerDepth, sourceColumns, utilityColumnCount]);
1647
1650
  const activeCustomHeaderLevels = (0, import_react5.useMemo)(() => {
1648
1651
  const levels = /* @__PURE__ */ new Map();
@@ -3348,9 +3351,15 @@ function Table({
3348
3351
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
3349
3352
  scheduleDraw();
3350
3353
  }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
3354
+ const isCellEditable = (0, import_react5.useCallback)((cell) => {
3355
+ const column = columns[cell.columnIndex];
3356
+ const row = rows[cell.rowIndex];
3357
+ if (!column || !row || column.dataIndex === void 0 || column.rowNumber || column.rowSelection || column.rowDragHandle) return false;
3358
+ return typeof column.editable === "function" ? column.editable(row[column.dataIndex], row, cell.rowIndex) : column.editable === true;
3359
+ }, [columns, rows]);
3351
3360
  const beginEdit = (0, import_react5.useCallback)((cell) => {
3352
3361
  const column = columns[cell.columnIndex];
3353
- if (!column?.editable || column.dataIndex === void 0) return;
3362
+ if (!isCellEditable(cell) || column.dataIndex === void 0) return;
3354
3363
  const value = rows[cell.rowIndex]?.[column.dataIndex];
3355
3364
  setDraft(value == null ? "" : String(value));
3356
3365
  const scroller = scrollerRef.current;
@@ -3392,7 +3401,7 @@ function Table({
3392
3401
  return;
3393
3402
  }
3394
3403
  setEditing(cell);
3395
- }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3404
+ }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, isCellEditable, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3396
3405
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
3397
3406
  const markCellEdited = (cell, previousValue, nextValue) => {
3398
3407
  const key = getCellEditKey(cell);
@@ -3420,6 +3429,10 @@ function Table({
3420
3429
  };
3421
3430
  const commitEdit = (0, import_react5.useCallback)((nextDraft, close = true) => {
3422
3431
  if (!editing) return;
3432
+ if (!isCellEditable(editing)) {
3433
+ setEditing(null);
3434
+ return;
3435
+ }
3423
3436
  const committedDraft = nextDraft ?? draft;
3424
3437
  const column = columns[editing.columnIndex];
3425
3438
  const row = rows[editing.rowIndex];
@@ -3434,7 +3447,7 @@ function Table({
3434
3447
  onCellChange?.({ ...editing, row, value: committedValue, previousValue });
3435
3448
  }
3436
3449
  if (close) setEditing(null);
3437
- }, [columns, draft, editing, onCellChange, rows]);
3450
+ }, [columns, draft, editing, isCellEditable, onCellChange, rows]);
3438
3451
  const renderEditor = () => {
3439
3452
  if (!editing || !editorStyle) return null;
3440
3453
  const column = columns[editing.columnIndex];
@@ -4336,6 +4349,77 @@ function Table({
4336
4349
  `summary:${column.key}`
4337
4350
  );
4338
4351
  };
4352
+ (0, import_react5.useImperativeHandle)(ref, () => {
4353
+ const resolveDataCell = (target) => {
4354
+ const cell = resolveSelectionTarget(target);
4355
+ if (!cell) return null;
4356
+ const column = columns[cell.columnIndex];
4357
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return null;
4358
+ const span = getCellSpan(cell.rowIndex, cell.columnIndex);
4359
+ if (!span) return cell;
4360
+ return {
4361
+ rowIndex: span.rowIndex,
4362
+ columnIndex: span.columnIndex,
4363
+ rowKey: getRowKey(rows[span.rowIndex], span.rowIndex),
4364
+ columnKey: columns[span.columnIndex].key
4365
+ };
4366
+ };
4367
+ return {
4368
+ focus: () => scrollerRef.current?.focus({ preventScroll: true }),
4369
+ scrollToCell: (target) => {
4370
+ const cell = resolveDataCell(target);
4371
+ if (!cell) return false;
4372
+ revealCell(cell);
4373
+ return true;
4374
+ },
4375
+ getSelectedCell: () => {
4376
+ if (!selection) return null;
4377
+ const cell = resolveDataCell(selection);
4378
+ if (!cell) return null;
4379
+ const column = columns[cell.columnIndex];
4380
+ const row = rows[cell.rowIndex];
4381
+ return { ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] };
4382
+ },
4383
+ setSelectedCell: (target) => {
4384
+ const cell = target === null ? null : resolveDataCell(target);
4385
+ if (target !== null && !cell) return false;
4386
+ setSelectionRange(null);
4387
+ setSelection(cell);
4388
+ return true;
4389
+ },
4390
+ getSelectedRowKeys: () => [...rowKeys],
4391
+ getSelectedRows: () => rows.filter((row, index) => selectedRowKeySet.has(getRowKey(row, index))),
4392
+ setSelectedRowKeys: (keys) => {
4393
+ if (!rowSelection) return;
4394
+ const next = [...new Set(keys)];
4395
+ setRowKeys(rowSelectionMode === "single" ? next.slice(0, 1) : next);
4396
+ },
4397
+ getSelectedColumnKeys: () => [...columnKeys],
4398
+ setSelectedColumnKeys: (keys) => {
4399
+ if (!columnSelection) return;
4400
+ const next = [...new Set(keys)].filter((key) => columns.some((column) => column.key === key && !column.rowNumber && !column.rowSelection && !column.rowDragHandle));
4401
+ setColumnKeys(typeof columnSelection === "object" && columnSelection.mode === "single" ? next.slice(0, 1) : next);
4402
+ },
4403
+ clearSelection: () => {
4404
+ setSelectionRange(null);
4405
+ setSelection(null);
4406
+ setRowKeys([]);
4407
+ setColumnKeys([]);
4408
+ rowAnchorRef.current = null;
4409
+ setEditing(null);
4410
+ },
4411
+ startEdit: (target) => {
4412
+ const cell = target ? resolveDataCell(target) : selection ? resolveDataCell(selection) : null;
4413
+ if (!cell || !isCellEditable(cell)) return false;
4414
+ setSelectionRange(null);
4415
+ setSelection(cell);
4416
+ beginEdit(cell);
4417
+ return true;
4418
+ },
4419
+ commitEdit: () => commitEdit(),
4420
+ cancelEdit: () => setEditing(null)
4421
+ };
4422
+ });
4339
4423
  const rootStyle = {
4340
4424
  width,
4341
4425
  height: autoHeight ? effectiveHeight : height,
@@ -4534,6 +4618,10 @@ function Table({
4534
4618
  }
4535
4619
  if (event.detail > 1) return;
4536
4620
  clickedCellRef.current = cell;
4621
+ const column = columns[cell.columnIndex];
4622
+ const row = rows[cell.rowIndex];
4623
+ onCellClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4624
+ if (event.defaultPrevented) return;
4537
4625
  setSelectionRange(null);
4538
4626
  scrollerRef.current?.focus({ preventScroll: true });
4539
4627
  setSelection(cell);
@@ -4544,7 +4632,12 @@ function Table({
4544
4632
  },
4545
4633
  onDoubleClick: (event) => {
4546
4634
  const cell = clickedCellRef.current ?? locateCell(event.clientX, event.clientY);
4547
- if (cell) beginEdit(cell);
4635
+ if (!cell) return;
4636
+ const column = columns[cell.columnIndex];
4637
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return;
4638
+ const row = rows[cell.rowIndex];
4639
+ onCellDoubleClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4640
+ if (!event.defaultPrevented) beginEdit(cell);
4548
4641
  },
4549
4642
  onContextMenu: handleContextMenu
4550
4643
  }
@@ -4557,7 +4650,7 @@ function Table({
4557
4650
  className: "rvg-text-scroll-content",
4558
4651
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
4559
4652
  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),
4653
+ 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
4654
  headerCells.map((cell) => {
4562
4655
  if (columns[cell.startIndex]?.fixed) return null;
4563
4656
  if (cell.endIndex < domRange.columnStart || cell.startIndex >= domRange.columnEnd) return null;
@@ -4575,7 +4668,7 @@ function Table({
4575
4668
  const left = column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
4576
4669
  return renderCellText(rowIndex, columnIndex, left);
4577
4670
  })) }),
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),
4671
+ 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
4672
  headerCells.map((cell) => columns[cell.startIndex]?.fixed ? renderGroupedHeaderCell(cell) : null),
4580
4673
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-shadow is-left", style: { left: fixedWidth } }),
4581
4674
  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 +4885,7 @@ function Table({
4792
4885
  const affectedRows = selectedIndices.includes(cell.rowIndex) ? selectedIndices : [cell.rowIndex];
4793
4886
  const cellContext = { ...cell, row, column, value: dataIndex === void 0 ? void 0 : row[dataIndex] };
4794
4887
  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" }) });
4888
+ 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
4889
  if (item === "copy") return renderContextMenuButton("cell-copy", cellContext, labels.copyContent, async () => {
4797
4890
  try {
4798
4891
  await navigator.clipboard.writeText(getCellLabel(cell.rowIndex, cell.columnIndex));
@@ -5032,6 +5125,7 @@ function Table({
5032
5125
  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
5126
  ] });
5034
5127
  }
5128
+ var Table = (0, import_react5.forwardRef)(TableInner);
5035
5129
  // Annotate the CommonJS export names for ESM import in node:
5036
5130
  0 && (module.exports = {
5037
5131
  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,7 @@ 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 showRowCheckbox = Boolean(rowSelection) && (typeof rowSelection !== "object" || rowSelection.showCheckbox !== false);
1593
1596
  useEffect2(() => {
1594
1597
  if (!isProductionRuntime() && hasNestedFixedColumns(sourceColumns)) {
1595
1598
  console.warn("[Table] Multi-level headers only support fixed columns on top-level columns. Nested fixed values are ignored.");
@@ -1600,19 +1603,19 @@ function Table({
1600
1603
  if (rowDraggable) {
1601
1604
  utilityColumns.push({ key: "__rvg_row_drag__", title: "", width: 36, align: "center", fixed: "left", rowDragHandle: true });
1602
1605
  }
1603
- if (rowSelection) {
1606
+ if (showRowCheckbox) {
1604
1607
  utilityColumns.push({ key: "__rvg_row_selection__", title: "", width: 44, align: "center", fixed: "left", rowSelection: true });
1605
1608
  }
1606
1609
  if (rowNumber) {
1607
- utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1610
+ 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
1611
  }
1609
1612
  const dataColumns = leafSourceColumns.map((column) => {
1610
1613
  const resizedWidth = resizedColumnWidths[column.key];
1611
1614
  return resizedWidth === void 0 ? column : { ...column, width: resizedWidth };
1612
1615
  });
1613
1616
  return [...utilityColumns, ...dataColumns];
1614
- }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, rowSelection, sourceRows.length]);
1615
- const utilityColumnCount = (rowDraggable ? 1 : 0) + (rowSelection ? 1 : 0) + (rowNumber ? 1 : 0);
1617
+ }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, showRowCheckbox, sourceRows.length]);
1618
+ const utilityColumnCount = (rowDraggable ? 1 : 0) + (showRowCheckbox ? 1 : 0) + (rowNumber ? 1 : 0);
1616
1619
  const headerCells = useMemo(() => buildHeaderCells(sourceColumns, utilityColumnCount, headerDepth), [headerDepth, sourceColumns, utilityColumnCount]);
1617
1620
  const activeCustomHeaderLevels = useMemo(() => {
1618
1621
  const levels = /* @__PURE__ */ new Map();
@@ -3318,9 +3321,15 @@ function Table({
3318
3321
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
3319
3322
  scheduleDraw();
3320
3323
  }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
3324
+ const isCellEditable = useCallback2((cell) => {
3325
+ const column = columns[cell.columnIndex];
3326
+ const row = rows[cell.rowIndex];
3327
+ if (!column || !row || column.dataIndex === void 0 || column.rowNumber || column.rowSelection || column.rowDragHandle) return false;
3328
+ return typeof column.editable === "function" ? column.editable(row[column.dataIndex], row, cell.rowIndex) : column.editable === true;
3329
+ }, [columns, rows]);
3321
3330
  const beginEdit = useCallback2((cell) => {
3322
3331
  const column = columns[cell.columnIndex];
3323
- if (!column?.editable || column.dataIndex === void 0) return;
3332
+ if (!isCellEditable(cell) || column.dataIndex === void 0) return;
3324
3333
  const value = rows[cell.rowIndex]?.[column.dataIndex];
3325
3334
  setDraft(value == null ? "" : String(value));
3326
3335
  const scroller = scrollerRef.current;
@@ -3362,7 +3371,7 @@ function Table({
3362
3371
  return;
3363
3372
  }
3364
3373
  setEditing(cell);
3365
- }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3374
+ }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, isCellEditable, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
3366
3375
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
3367
3376
  const markCellEdited = (cell, previousValue, nextValue) => {
3368
3377
  const key = getCellEditKey(cell);
@@ -3390,6 +3399,10 @@ function Table({
3390
3399
  };
3391
3400
  const commitEdit = useCallback2((nextDraft, close = true) => {
3392
3401
  if (!editing) return;
3402
+ if (!isCellEditable(editing)) {
3403
+ setEditing(null);
3404
+ return;
3405
+ }
3393
3406
  const committedDraft = nextDraft ?? draft;
3394
3407
  const column = columns[editing.columnIndex];
3395
3408
  const row = rows[editing.rowIndex];
@@ -3404,7 +3417,7 @@ function Table({
3404
3417
  onCellChange?.({ ...editing, row, value: committedValue, previousValue });
3405
3418
  }
3406
3419
  if (close) setEditing(null);
3407
- }, [columns, draft, editing, onCellChange, rows]);
3420
+ }, [columns, draft, editing, isCellEditable, onCellChange, rows]);
3408
3421
  const renderEditor = () => {
3409
3422
  if (!editing || !editorStyle) return null;
3410
3423
  const column = columns[editing.columnIndex];
@@ -4306,6 +4319,77 @@ function Table({
4306
4319
  `summary:${column.key}`
4307
4320
  );
4308
4321
  };
4322
+ useImperativeHandle(ref, () => {
4323
+ const resolveDataCell = (target) => {
4324
+ const cell = resolveSelectionTarget(target);
4325
+ if (!cell) return null;
4326
+ const column = columns[cell.columnIndex];
4327
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return null;
4328
+ const span = getCellSpan(cell.rowIndex, cell.columnIndex);
4329
+ if (!span) return cell;
4330
+ return {
4331
+ rowIndex: span.rowIndex,
4332
+ columnIndex: span.columnIndex,
4333
+ rowKey: getRowKey(rows[span.rowIndex], span.rowIndex),
4334
+ columnKey: columns[span.columnIndex].key
4335
+ };
4336
+ };
4337
+ return {
4338
+ focus: () => scrollerRef.current?.focus({ preventScroll: true }),
4339
+ scrollToCell: (target) => {
4340
+ const cell = resolveDataCell(target);
4341
+ if (!cell) return false;
4342
+ revealCell(cell);
4343
+ return true;
4344
+ },
4345
+ getSelectedCell: () => {
4346
+ if (!selection) return null;
4347
+ const cell = resolveDataCell(selection);
4348
+ if (!cell) return null;
4349
+ const column = columns[cell.columnIndex];
4350
+ const row = rows[cell.rowIndex];
4351
+ return { ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] };
4352
+ },
4353
+ setSelectedCell: (target) => {
4354
+ const cell = target === null ? null : resolveDataCell(target);
4355
+ if (target !== null && !cell) return false;
4356
+ setSelectionRange(null);
4357
+ setSelection(cell);
4358
+ return true;
4359
+ },
4360
+ getSelectedRowKeys: () => [...rowKeys],
4361
+ getSelectedRows: () => rows.filter((row, index) => selectedRowKeySet.has(getRowKey(row, index))),
4362
+ setSelectedRowKeys: (keys) => {
4363
+ if (!rowSelection) return;
4364
+ const next = [...new Set(keys)];
4365
+ setRowKeys(rowSelectionMode === "single" ? next.slice(0, 1) : next);
4366
+ },
4367
+ getSelectedColumnKeys: () => [...columnKeys],
4368
+ setSelectedColumnKeys: (keys) => {
4369
+ if (!columnSelection) return;
4370
+ const next = [...new Set(keys)].filter((key) => columns.some((column) => column.key === key && !column.rowNumber && !column.rowSelection && !column.rowDragHandle));
4371
+ setColumnKeys(typeof columnSelection === "object" && columnSelection.mode === "single" ? next.slice(0, 1) : next);
4372
+ },
4373
+ clearSelection: () => {
4374
+ setSelectionRange(null);
4375
+ setSelection(null);
4376
+ setRowKeys([]);
4377
+ setColumnKeys([]);
4378
+ rowAnchorRef.current = null;
4379
+ setEditing(null);
4380
+ },
4381
+ startEdit: (target) => {
4382
+ const cell = target ? resolveDataCell(target) : selection ? resolveDataCell(selection) : null;
4383
+ if (!cell || !isCellEditable(cell)) return false;
4384
+ setSelectionRange(null);
4385
+ setSelection(cell);
4386
+ beginEdit(cell);
4387
+ return true;
4388
+ },
4389
+ commitEdit: () => commitEdit(),
4390
+ cancelEdit: () => setEditing(null)
4391
+ };
4392
+ });
4309
4393
  const rootStyle = {
4310
4394
  width,
4311
4395
  height: autoHeight ? effectiveHeight : height,
@@ -4504,6 +4588,10 @@ function Table({
4504
4588
  }
4505
4589
  if (event.detail > 1) return;
4506
4590
  clickedCellRef.current = cell;
4591
+ const column = columns[cell.columnIndex];
4592
+ const row = rows[cell.rowIndex];
4593
+ onCellClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4594
+ if (event.defaultPrevented) return;
4507
4595
  setSelectionRange(null);
4508
4596
  scrollerRef.current?.focus({ preventScroll: true });
4509
4597
  setSelection(cell);
@@ -4514,7 +4602,12 @@ function Table({
4514
4602
  },
4515
4603
  onDoubleClick: (event) => {
4516
4604
  const cell = clickedCellRef.current ?? locateCell(event.clientX, event.clientY);
4517
- if (cell) beginEdit(cell);
4605
+ if (!cell) return;
4606
+ const column = columns[cell.columnIndex];
4607
+ if (column.rowNumber || column.rowSelection || column.rowDragHandle) return;
4608
+ const row = rows[cell.rowIndex];
4609
+ onCellDoubleClick?.({ ...cell, row, column, value: column.dataIndex === void 0 ? void 0 : row[column.dataIndex] }, event);
4610
+ if (!event.defaultPrevented) beginEdit(cell);
4518
4611
  },
4519
4612
  onContextMenu: handleContextMenu
4520
4613
  }
@@ -4527,7 +4620,7 @@ function Table({
4527
4620
  className: "rvg-text-scroll-content",
4528
4621
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
4529
4622
  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),
4623
+ 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
4624
  headerCells.map((cell) => {
4532
4625
  if (columns[cell.startIndex]?.fixed) return null;
4533
4626
  if (cell.endIndex < domRange.columnStart || cell.startIndex >= domRange.columnEnd) return null;
@@ -4545,7 +4638,7 @@ function Table({
4545
4638
  const left = column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
4546
4639
  return renderCellText(rowIndex, columnIndex, left);
4547
4640
  })) }),
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),
4641
+ 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
4642
  headerCells.map((cell) => columns[cell.startIndex]?.fixed ? renderGroupedHeaderCell(cell) : null),
4550
4643
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-shadow is-left", style: { left: fixedWidth } }),
4551
4644
  rightFixedWidth > 0 && currentScroll.left < contentWidth - viewport.width && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-shadow is-right", style: { right: rightFixedWidth } }),
@@ -4762,7 +4855,7 @@ function Table({
4762
4855
  const affectedRows = selectedIndices.includes(cell.rowIndex) ? selectedIndices : [cell.rowIndex];
4763
4856
  const cellContext = { ...cell, row, column, value: dataIndex === void 0 ? void 0 : row[dataIndex] };
4764
4857
  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" }) });
4858
+ if (item === "edit") return renderContextMenuButton("cell-edit", cellContext, labels.edit, () => beginEdit(cell), override, { disabled: !isCellEditable(cell), icon: /* @__PURE__ */ jsx7(ContextMenuIcon, { type: "edit" }) });
4766
4859
  if (item === "copy") return renderContextMenuButton("cell-copy", cellContext, labels.copyContent, async () => {
4767
4860
  try {
4768
4861
  await navigator.clipboard.writeText(getCellLabel(cell.rowIndex, cell.columnIndex));
@@ -5002,6 +5095,7 @@ function Table({
5002
5095
  loading && !hasCustomLoading && hasCompletedLoadRef.current && /* @__PURE__ */ jsx7("div", { className: "rvg-refresh-loading", "aria-label": labels.refreshing, children: /* @__PURE__ */ jsx7(LoadingSpinnerIcon, {}) })
5003
5096
  ] });
5004
5097
  }
5098
+ var Table = forwardRef(TableInner);
5005
5099
  export {
5006
5100
  Table,
5007
5101
  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.7",
4
4
  "description": "A lightweight, canvas-rendered React table component for LensUI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",