@zvndev/yable-react 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -631,8 +631,10 @@ interface TableBodyProps<TData extends RowData> {
631
631
  table: Table$1<TData>;
632
632
  clickableRows?: boolean;
633
633
  colgroup?: React__default.ReactNode;
634
+ /** Stable mousedown handler for the fill handle, lifted from `useFillHandle`. */
635
+ onFillHandleMouseDown?: (rowIndex: number, columnIndex: number, e: React__default.MouseEvent) => void;
634
636
  }
635
- declare function TableBody<TData extends RowData>({ table, clickableRows, colgroup, }: TableBodyProps<TData>): react_jsx_runtime.JSX.Element;
637
+ declare function TableBody<TData extends RowData>({ table, clickableRows, colgroup, onFillHandleMouseDown, }: TableBodyProps<TData>): react_jsx_runtime.JSX.Element;
636
638
 
637
639
  interface TableCellProps<TData extends RowData> {
638
640
  cell: Cell<TData, unknown>;
@@ -641,8 +643,11 @@ interface TableCellProps<TData extends RowData> {
641
643
  columnIndex: number;
642
644
  isFocused: boolean;
643
645
  isTabStop: boolean;
646
+ /** Mousedown handler for the fill handle; when present and the table has
647
+ * `enableFillHandle`, the focused cell renders a drag-to-fill corner. */
648
+ onFillHandleMouseDown?: (rowIndex: number, columnIndex: number, e: React__default.MouseEvent) => void;
644
649
  }
645
- declare function TableCell<TData extends RowData>({ cell, table, rowIndex, columnIndex, isFocused, isTabStop, }: TableCellProps<TData>): react_jsx_runtime.JSX.Element;
650
+ declare function TableCell<TData extends RowData>({ cell, table, rowIndex, columnIndex, isFocused, isTabStop, onFillHandleMouseDown, }: TableCellProps<TData>): react_jsx_runtime.JSX.Element;
646
651
 
647
652
  interface TableFooterProps<TData extends RowData> {
648
653
  table: Table$1<TData>;
package/dist/index.d.ts CHANGED
@@ -631,8 +631,10 @@ interface TableBodyProps<TData extends RowData> {
631
631
  table: Table$1<TData>;
632
632
  clickableRows?: boolean;
633
633
  colgroup?: React__default.ReactNode;
634
+ /** Stable mousedown handler for the fill handle, lifted from `useFillHandle`. */
635
+ onFillHandleMouseDown?: (rowIndex: number, columnIndex: number, e: React__default.MouseEvent) => void;
634
636
  }
635
- declare function TableBody<TData extends RowData>({ table, clickableRows, colgroup, }: TableBodyProps<TData>): react_jsx_runtime.JSX.Element;
637
+ declare function TableBody<TData extends RowData>({ table, clickableRows, colgroup, onFillHandleMouseDown, }: TableBodyProps<TData>): react_jsx_runtime.JSX.Element;
636
638
 
637
639
  interface TableCellProps<TData extends RowData> {
638
640
  cell: Cell<TData, unknown>;
@@ -641,8 +643,11 @@ interface TableCellProps<TData extends RowData> {
641
643
  columnIndex: number;
642
644
  isFocused: boolean;
643
645
  isTabStop: boolean;
646
+ /** Mousedown handler for the fill handle; when present and the table has
647
+ * `enableFillHandle`, the focused cell renders a drag-to-fill corner. */
648
+ onFillHandleMouseDown?: (rowIndex: number, columnIndex: number, e: React__default.MouseEvent) => void;
644
649
  }
645
- declare function TableCell<TData extends RowData>({ cell, table, rowIndex, columnIndex, isFocused, isTabStop, }: TableCellProps<TData>): react_jsx_runtime.JSX.Element;
650
+ declare function TableCell<TData extends RowData>({ cell, table, rowIndex, columnIndex, isFocused, isTabStop, onFillHandleMouseDown, }: TableCellProps<TData>): react_jsx_runtime.JSX.Element;
646
651
 
647
652
  interface TableFooterProps<TData extends RowData> {
648
653
  table: Table$1<TData>;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { canCellEnterEditMode, functionalUpdate, createTable, getDefaultLocale, getFirstKeyboardCell, getResolvedFocusedCell, detectCellChanges } from '@zvndev/yable-core';
2
2
  export { CommitError, FormulaEngine, FormulaError, PivotEngine, UndoStack, aggregationFns, createColumnHelper, createLocale, createUndoRedoIntegration, en, filterFns, formulaFunctions, functionalUpdate, generatePivotColumnDefs, getDefaultLocale, getInitialPivotState, getPivotRowModel, resetLocale, setDefaultLocale, sortingFns } from '@zvndev/yable-core';
3
- import React3, { createContext, useCallback, useMemo, useContext, useState, useRef, useEffect } from 'react';
3
+ import React4, { createContext, useCallback, useMemo, useContext, useState, useRef, useEffect } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { getInitialRowDragState, moveRow } from '@zvndev/yable-core/features/rowDragging';
@@ -2188,13 +2188,39 @@ function CellStatusBadge(props) {
2188
2188
  }
2189
2189
  );
2190
2190
  }
2191
+ function FillHandle({
2192
+ rowIndex,
2193
+ columnIndex,
2194
+ onMouseDown
2195
+ }) {
2196
+ const handleMouseDown = useCallback(
2197
+ (e) => {
2198
+ e.preventDefault();
2199
+ e.stopPropagation();
2200
+ onMouseDown(rowIndex, columnIndex, e);
2201
+ },
2202
+ [rowIndex, columnIndex, onMouseDown]
2203
+ );
2204
+ return /* @__PURE__ */ jsx(
2205
+ "div",
2206
+ {
2207
+ className: "yable-fill-handle",
2208
+ onMouseDown: handleMouseDown,
2209
+ role: "presentation",
2210
+ "aria-hidden": "true",
2211
+ title: "Drag to fill",
2212
+ children: /* @__PURE__ */ jsx("div", { className: "yable-fill-handle-dot" })
2213
+ }
2214
+ );
2215
+ }
2191
2216
  function TableCell({
2192
2217
  cell,
2193
2218
  table,
2194
2219
  rowIndex,
2195
2220
  columnIndex,
2196
2221
  isFocused,
2197
- isTabStop
2222
+ isTabStop,
2223
+ onFillHandleMouseDown
2198
2224
  }) {
2199
2225
  const column = cell.column;
2200
2226
  const isEditing = cell.getIsEditing();
@@ -2243,8 +2269,42 @@ function TableCell({
2243
2269
  } else {
2244
2270
  content = overrideValue !== void 0 ? overrideValue : cell.renderValue();
2245
2271
  }
2272
+ const isGroupRow = cell.row.getIsGrouped();
2273
+ if (isGroupRow) {
2274
+ if (column.id === cell.row.groupingColumnId) {
2275
+ const expanded = cell.row.getIsExpanded();
2276
+ content = /* @__PURE__ */ jsxs("span", { className: "yable-group-cell", style: { paddingLeft: cell.row.depth * 16 }, children: [
2277
+ /* @__PURE__ */ jsx(
2278
+ "button",
2279
+ {
2280
+ type: "button",
2281
+ className: "yable-group-toggle",
2282
+ "aria-label": expanded ? "Collapse group" : "Expand group",
2283
+ "aria-expanded": expanded,
2284
+ onClick: cell.row.getToggleExpandedHandler(),
2285
+ children: expanded ? "\u25BE" : "\u25B8"
2286
+ }
2287
+ ),
2288
+ /* @__PURE__ */ jsx("span", { className: "yable-group-value", children: String(cell.row.groupingValue ?? "") }),
2289
+ /* @__PURE__ */ jsxs("span", { className: "yable-group-count", children: [
2290
+ "(",
2291
+ cell.row.getLeafRows().length,
2292
+ ")"
2293
+ ] })
2294
+ ] });
2295
+ } else {
2296
+ const aggDef = column.columnDef.aggregatedCell;
2297
+ if (typeof aggDef === "function") {
2298
+ content = aggDef(cell.getContext());
2299
+ } else {
2300
+ const aggVal = cell.getValue();
2301
+ content = aggVal == null ? null : aggVal;
2302
+ }
2303
+ }
2304
+ }
2246
2305
  const handleClick = useCallback(
2247
2306
  (e) => {
2307
+ if (cell.row.getIsGrouped()) return;
2248
2308
  table.events.emit("cell:click", {
2249
2309
  cell,
2250
2310
  row: cell.row,
@@ -2310,6 +2370,7 @@ function TableCell({
2310
2370
  const cellStyleDef = column.columnDef.cellStyle;
2311
2371
  const userStyle = typeof cellStyleDef === "function" ? cellStyleDef(cell.getContext()) : cellStyleDef;
2312
2372
  const mergedStyle = userStyle ? { ...style, ...userStyle } : style;
2373
+ const showFillHandle = isFocused && Boolean(table.options.enableFillHandle) && onFillHandleMouseDown != null && !isGroupRow;
2313
2374
  const classNames = [
2314
2375
  "yable-td",
2315
2376
  isFocused && "yable-cell--focused",
@@ -2330,6 +2391,7 @@ function TableCell({
2330
2391
  "data-pinned": pinned || void 0,
2331
2392
  "data-cell-status": cellStatus !== "idle" ? cellStatus : void 0,
2332
2393
  "data-column-id": column.id,
2394
+ "data-grouped": isGroupRow || void 0,
2333
2395
  "data-row-index": rowIndex,
2334
2396
  "data-column-index": columnIndex,
2335
2397
  "data-cell-selected": isCellSelected || void 0,
@@ -2363,6 +2425,14 @@ function TableCell({
2363
2425
  onRetry: () => void table.retryCommit(cell.row.id, column.id),
2364
2426
  onDismiss: () => table.dismissCommit(cell.row.id, column.id)
2365
2427
  }
2428
+ ),
2429
+ showFillHandle && onFillHandleMouseDown && /* @__PURE__ */ jsx(
2430
+ FillHandle,
2431
+ {
2432
+ rowIndex,
2433
+ columnIndex,
2434
+ onMouseDown: onFillHandleMouseDown
2435
+ }
2366
2436
  )
2367
2437
  ]
2368
2438
  }
@@ -2375,7 +2445,7 @@ function isInteractiveClickTarget(element) {
2375
2445
  );
2376
2446
  return interactive !== null;
2377
2447
  }
2378
- var ErrorBoundary = class extends React3.Component {
2448
+ var ErrorBoundary = class extends React4.Component {
2379
2449
  constructor(props) {
2380
2450
  super(props);
2381
2451
  this.state = { hasError: false, error: null };
@@ -2421,7 +2491,7 @@ var ErrorBoundary = class extends React3.Component {
2421
2491
  return this.props.children;
2422
2492
  }
2423
2493
  };
2424
- var CellErrorBoundary = class extends React3.Component {
2494
+ var CellErrorBoundary = class extends React4.Component {
2425
2495
  constructor(props) {
2426
2496
  super(props);
2427
2497
  this.state = { hasError: false, error: null };
@@ -2494,7 +2564,8 @@ function MasterDetail({
2494
2564
  function TableBody({
2495
2565
  table,
2496
2566
  clickableRows,
2497
- colgroup
2567
+ colgroup,
2568
+ onFillHandleMouseDown
2498
2569
  }) {
2499
2570
  const rows = table.getRowModel().rows;
2500
2571
  const visibleColumns = table.getVisibleLeafColumns();
@@ -2563,7 +2634,8 @@ function TableBody({
2563
2634
  cellSelectionKey,
2564
2635
  pendingValuesKey: getPendingValuesKey(pendingValues[row.id]),
2565
2636
  clickable: clickableRows,
2566
- pinnedPosition
2637
+ pinnedPosition,
2638
+ onFillHandleMouseDown
2567
2639
  },
2568
2640
  row.id
2569
2641
  );
@@ -2632,6 +2704,7 @@ function TableBody({
2632
2704
  cellSelectionKey,
2633
2705
  pendingValuesKey: getPendingValuesKey(pendingValues[row.id]),
2634
2706
  clickable: clickableRows,
2707
+ onFillHandleMouseDown,
2635
2708
  virtualStyle: {
2636
2709
  position: "absolute",
2637
2710
  top: 0,
@@ -2666,7 +2739,8 @@ function TableRowInner({
2666
2739
  pendingValuesKey: _pendingValuesKey,
2667
2740
  clickable,
2668
2741
  pinnedPosition,
2669
- virtualStyle
2742
+ virtualStyle,
2743
+ onFillHandleMouseDown
2670
2744
  }) {
2671
2745
  const allCells = row.getAllCells();
2672
2746
  const visibleCells = visibleColumns.map((column) => allCells.find((cell) => cell.column.id === column.id)).filter((cell) => cell != null);
@@ -2746,7 +2820,8 @@ function TableRowInner({
2746
2820
  rowIndex,
2747
2821
  columnIndex,
2748
2822
  isFocused,
2749
- isTabStop
2823
+ isTabStop,
2824
+ onFillHandleMouseDown
2750
2825
  }
2751
2826
  )
2752
2827
  },
@@ -2780,7 +2855,7 @@ function areRowPropsEqual(prev, next) {
2780
2855
  if (prev.table !== next.table) return false;
2781
2856
  return true;
2782
2857
  }
2783
- var MemoizedTableRow = React3.memo(TableRowInner, areRowPropsEqual);
2858
+ var MemoizedTableRow = React4.memo(TableRowInner, areRowPropsEqual);
2784
2859
  function getPendingValuesKey(values) {
2785
2860
  if (!values) return "";
2786
2861
  return Object.keys(values).sort().map((key) => `${key}:${String(values[key])}`).join("|");
@@ -2998,8 +3073,8 @@ function StatusDivider() {
2998
3073
  return /* @__PURE__ */ jsx("span", { className: "yable-status-bar-divider", "aria-hidden": "true" });
2999
3074
  }
3000
3075
  function PanelGroup({ children }) {
3001
- const items = React3.Children.toArray(children).filter(Boolean);
3002
- return /* @__PURE__ */ jsx(Fragment, { children: items.map((child, i) => /* @__PURE__ */ jsxs(React3.Fragment, { children: [
3076
+ const items = React4.Children.toArray(children).filter(Boolean);
3077
+ return /* @__PURE__ */ jsx(Fragment, { children: items.map((child, i) => /* @__PURE__ */ jsxs(React4.Fragment, { children: [
3003
3078
  i > 0 && /* @__PURE__ */ jsx(StatusDivider, {}),
3004
3079
  child
3005
3080
  ] }, i)) });
@@ -4002,6 +4077,85 @@ function isEditableTarget(element) {
4002
4077
  }
4003
4078
  return element.isContentEditable;
4004
4079
  }
4080
+ function useFillHandle(table, options = {}) {
4081
+ const { enabled = true } = options;
4082
+ const [dragState, setDragState] = useState({
4083
+ isDragging: false,
4084
+ sourceCell: null,
4085
+ currentCell: null
4086
+ });
4087
+ const dragRef = useRef(dragState);
4088
+ dragRef.current = dragState;
4089
+ const onFillHandleMouseDown = useCallback(
4090
+ (rowIndex, columnIndex, e) => {
4091
+ if (!enabled) return;
4092
+ e.preventDefault();
4093
+ e.stopPropagation();
4094
+ setDragState({
4095
+ isDragging: true,
4096
+ sourceCell: { rowIndex, columnIndex },
4097
+ currentCell: { rowIndex, columnIndex }
4098
+ });
4099
+ },
4100
+ [enabled]
4101
+ );
4102
+ useEffect(() => {
4103
+ if (!dragState.isDragging) return;
4104
+ const handleMouseMove = (e) => {
4105
+ const target = document.elementFromPoint(e.clientX, e.clientY);
4106
+ if (!target) return;
4107
+ const td = target.closest("td[data-column-id]");
4108
+ const tr = target.closest("tr[data-row-id]");
4109
+ if (!td || !tr) return;
4110
+ const columnId = td.getAttribute("data-column-id");
4111
+ const rowId = tr.getAttribute("data-row-id");
4112
+ if (!columnId || !rowId) return;
4113
+ const rows = table.getRowModel().rows;
4114
+ const columns = table.getVisibleLeafColumns();
4115
+ const rowIndex = rows.findIndex((r) => r.id === rowId);
4116
+ const columnIndex = columns.findIndex((c) => c.id === columnId);
4117
+ if (rowIndex === -1 || columnIndex === -1) return;
4118
+ setDragState((prev) => ({
4119
+ ...prev,
4120
+ currentCell: { rowIndex, columnIndex }
4121
+ }));
4122
+ };
4123
+ const handleMouseUp = () => {
4124
+ const current = dragRef.current;
4125
+ if (current.sourceCell && current.currentCell) {
4126
+ const source = current.sourceCell;
4127
+ const target = current.currentCell;
4128
+ if (source.rowIndex !== target.rowIndex || source.columnIndex !== target.columnIndex) {
4129
+ const sourceRange = {
4130
+ startRow: source.rowIndex,
4131
+ startCol: source.columnIndex,
4132
+ endRow: source.rowIndex,
4133
+ endCol: source.columnIndex
4134
+ };
4135
+ const targetRange = {
4136
+ startRow: Math.min(source.rowIndex, target.rowIndex),
4137
+ startCol: Math.min(source.columnIndex, target.columnIndex),
4138
+ endRow: Math.max(source.rowIndex, target.rowIndex),
4139
+ endCol: Math.max(source.columnIndex, target.columnIndex)
4140
+ };
4141
+ table.fillRange(sourceRange, targetRange);
4142
+ }
4143
+ }
4144
+ setDragState({
4145
+ isDragging: false,
4146
+ sourceCell: null,
4147
+ currentCell: null
4148
+ });
4149
+ };
4150
+ document.addEventListener("mousemove", handleMouseMove);
4151
+ document.addEventListener("mouseup", handleMouseUp);
4152
+ return () => {
4153
+ document.removeEventListener("mousemove", handleMouseMove);
4154
+ document.removeEventListener("mouseup", handleMouseUp);
4155
+ };
4156
+ }, [dragState.isDragging, table]);
4157
+ return { dragState, onFillHandleMouseDown };
4158
+ }
4005
4159
  function filterHeaderGroups(groups, visibleColumnIds) {
4006
4160
  return groups.map((group) => ({
4007
4161
  ...group,
@@ -4125,6 +4279,9 @@ function Table({
4125
4279
  const showColumnVirtualizationShell = canVirtualizeColumns;
4126
4280
  const contextMenu = useContextMenu();
4127
4281
  useKeyboardNavigation(table, { containerRef });
4282
+ const { onFillHandleMouseDown } = useFillHandle(table, {
4283
+ enabled: Boolean(table.options.enableFillHandle)
4284
+ });
4128
4285
  const [announcement, setAnnouncement] = useState("");
4129
4286
  const prevSortingRef = useRef(table.getState().sorting);
4130
4287
  const prevFilterCountRef = useRef(rows.length);
@@ -4212,7 +4369,15 @@ function Table({
4212
4369
  children: [
4213
4370
  colgroup,
4214
4371
  /* @__PURE__ */ jsx(TableHeader, { table: renderTable, floatingFilters: resolvedFloatingFilters }),
4215
- /* @__PURE__ */ jsx(TableBody, { table: renderTable, clickableRows: resolvedClickableRows, colgroup }),
4372
+ /* @__PURE__ */ jsx(
4373
+ TableBody,
4374
+ {
4375
+ table: renderTable,
4376
+ clickableRows: resolvedClickableRows,
4377
+ colgroup,
4378
+ onFillHandleMouseDown
4379
+ }
4380
+ ),
4216
4381
  footer && /* @__PURE__ */ jsx(TableFooter, { table: renderTable })
4217
4382
  ]
4218
4383
  }
@@ -4900,110 +5065,6 @@ function isEditableTarget2(el) {
4900
5065
  if (el.isContentEditable) return true;
4901
5066
  return false;
4902
5067
  }
4903
- function useFillHandle(table, options = {}) {
4904
- const { enabled = true } = options;
4905
- const [dragState, setDragState] = useState({
4906
- isDragging: false,
4907
- sourceCell: null,
4908
- currentCell: null
4909
- });
4910
- const dragRef = useRef(dragState);
4911
- dragRef.current = dragState;
4912
- const onFillHandleMouseDown = useCallback(
4913
- (rowIndex, columnIndex, e) => {
4914
- if (!enabled) return;
4915
- e.preventDefault();
4916
- e.stopPropagation();
4917
- setDragState({
4918
- isDragging: true,
4919
- sourceCell: { rowIndex, columnIndex },
4920
- currentCell: { rowIndex, columnIndex }
4921
- });
4922
- },
4923
- [enabled]
4924
- );
4925
- useEffect(() => {
4926
- if (!dragState.isDragging) return;
4927
- const handleMouseMove = (e) => {
4928
- const target = document.elementFromPoint(e.clientX, e.clientY);
4929
- if (!target) return;
4930
- const td = target.closest("td[data-column-id]");
4931
- const tr = target.closest("tr[data-row-id]");
4932
- if (!td || !tr) return;
4933
- const columnId = td.getAttribute("data-column-id");
4934
- const rowId = tr.getAttribute("data-row-id");
4935
- if (!columnId || !rowId) return;
4936
- const rows = table.getRowModel().rows;
4937
- const columns = table.getVisibleLeafColumns();
4938
- const rowIndex = rows.findIndex((r) => r.id === rowId);
4939
- const columnIndex = columns.findIndex((c) => c.id === columnId);
4940
- if (rowIndex === -1 || columnIndex === -1) return;
4941
- setDragState((prev) => ({
4942
- ...prev,
4943
- currentCell: { rowIndex, columnIndex }
4944
- }));
4945
- };
4946
- const handleMouseUp = () => {
4947
- const current = dragRef.current;
4948
- if (current.sourceCell && current.currentCell) {
4949
- const source = current.sourceCell;
4950
- const target = current.currentCell;
4951
- if (source.rowIndex !== target.rowIndex || source.columnIndex !== target.columnIndex) {
4952
- const sourceRange = {
4953
- startRow: source.rowIndex,
4954
- startCol: source.columnIndex,
4955
- endRow: source.rowIndex,
4956
- endCol: source.columnIndex
4957
- };
4958
- const targetRange = {
4959
- startRow: Math.min(source.rowIndex, target.rowIndex),
4960
- startCol: Math.min(source.columnIndex, target.columnIndex),
4961
- endRow: Math.max(source.rowIndex, target.rowIndex),
4962
- endCol: Math.max(source.columnIndex, target.columnIndex)
4963
- };
4964
- table.fillRange(sourceRange, targetRange);
4965
- }
4966
- }
4967
- setDragState({
4968
- isDragging: false,
4969
- sourceCell: null,
4970
- currentCell: null
4971
- });
4972
- };
4973
- document.addEventListener("mousemove", handleMouseMove);
4974
- document.addEventListener("mouseup", handleMouseUp);
4975
- return () => {
4976
- document.removeEventListener("mousemove", handleMouseMove);
4977
- document.removeEventListener("mouseup", handleMouseUp);
4978
- };
4979
- }, [dragState.isDragging, table]);
4980
- return { dragState, onFillHandleMouseDown };
4981
- }
4982
- function FillHandle({
4983
- rowIndex,
4984
- columnIndex,
4985
- onMouseDown
4986
- }) {
4987
- const handleMouseDown = useCallback(
4988
- (e) => {
4989
- e.preventDefault();
4990
- e.stopPropagation();
4991
- onMouseDown(rowIndex, columnIndex, e);
4992
- },
4993
- [rowIndex, columnIndex, onMouseDown]
4994
- );
4995
- return /* @__PURE__ */ jsx(
4996
- "div",
4997
- {
4998
- className: "yable-fill-handle",
4999
- onMouseDown: handleMouseDown,
5000
- role: "presentation",
5001
- "aria-hidden": "true",
5002
- title: "Drag to fill",
5003
- children: /* @__PURE__ */ jsx("div", { className: "yable-fill-handle-dot" })
5004
- }
5005
- );
5006
- }
5007
5068
  function GripIcon() {
5008
5069
  return /* @__PURE__ */ jsxs(
5009
5070
  "svg",