@ricsam/react-spreadsheets 0.0.10 → 0.0.12

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
@@ -339,6 +339,37 @@ Per-cell styling is done in JS:
339
339
  | `selection` | Selection state, navigation model, and `effects(selectionManager)` for copy/paste/fill/reference hooks. |
340
340
  | `components` + `overlayChildren` | Floating overlays anchored to the grid, optionally snapped to cell edges. |
341
341
 
342
+ Wheel input over the grid remains available when its cell selection is blurred.
343
+ Wheel events originating inside a floating overlay are left to that overlay, so
344
+ scrollable editors and other interactive overlay content keep their native
345
+ scroll behavior.
346
+
347
+ ## Accessibility and browser automation
348
+
349
+ Pass `aria-label`, `aria-labelledby`, and/or `aria-describedby` directly to
350
+ `Spreadsheet`, `FormulaSheet`, or `FormulaWorkbook` to name/describe the inner
351
+ keyboard grid. The default label is `Spreadsheet`; `containerProps` still targets
352
+ the outer wrapper. Tab focus activates the existing selection-manager keyboard
353
+ navigation without adding every virtual cell to the tab order.
354
+
355
+ Mounted cells expose `role="gridcell"`, `aria-selected`, and stable-within-view
356
+ IDs. Their accessible names come from rendered content (including custom
357
+ renderers); use `[data-cell-address="B2"]` for an unambiguous A1 locator. Inline
358
+ editors are named `Edit B2`. Headers expose `columnheader` / `rowheader` roles.
359
+ ARIA coordinates/counts include the header row and column: A1 is row 2, column 2;
360
+ unknown/unbounded counts are `-1`. Logical rows own their cells with `aria-owns`
361
+ because frozen headers and cells render in separate clipping layers.
362
+
363
+ Only mounted cells are exposed. Reveal offscreen cells with `scrollToCell`,
364
+ `scrollToRange`, or `WorkbookSelectionManager.focusRange` before locating them.
365
+ `aria-activedescendant` is omitted when the selection anchor is not mounted.
366
+ With selection-manager versions providing `setInputCaptureAccessibility`, the
367
+ same active-cell relationship and grid label are forwarded to its focused
368
+ keyboard-capture textarea. Older peers keep grid semantics and keyboard behavior
369
+ but do not announce this relationship from that textarea. This is not a claim
370
+ of complete screen-reader support; custom overlays and renderers must supply
371
+ their own accessible controls.
372
+
342
373
  ## Development
343
374
 
344
375
  ```bash
package/dist/cjs/lib.cjs CHANGED
@@ -18,7 +18,7 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  return cached;
19
19
  }
20
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
21
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
21
+ const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
22
  if (mod && typeof mod === "object" || typeof mod === "function") {
23
23
  for (let key of __getOwnPropNames(mod))
24
24
  if (!__hasOwnProp.call(to, key))
@@ -119,7 +119,7 @@ module.exports = __toCommonJS(exports_lib);
119
119
 
120
120
  // src/spreadsheet/spreadsheet.tsx
121
121
  var import_selection_manager = require("@ricsam/selection-manager");
122
- var import_react4 = __toESM(require("react"));
122
+ var import_react4 = __toESM(require("react"), 1);
123
123
 
124
124
  // src/utils/use-resize-observer.ts
125
125
  var import_react = require("react");
@@ -1434,6 +1434,7 @@ var resizeCellSnapAnchorToNearestEdges = (anchor, rect, handle, columnWidths, ro
1434
1434
 
1435
1435
  // src/spreadsheet/spreadsheet.tsx
1436
1436
  var jsx_runtime3 = require("react/jsx-runtime");
1437
+ var SPREADSHEET_OVERLAY_SELECTOR = "[data-rsp-overlay]";
1437
1438
  var resolveGridLineColor = (probe, colorScheme) => {
1438
1439
  const fallback = colorScheme === "dark" ? "#232c3d" : "#e8ebf1";
1439
1440
  if (!probe || typeof getComputedStyle !== "function") {
@@ -1579,6 +1580,7 @@ var CellComponent = import_react4.default.memo(({
1579
1580
  value,
1580
1581
  row,
1581
1582
  col,
1583
+ domId,
1582
1584
  selectionManager,
1583
1585
  cellToPixelBase,
1584
1586
  getColumnWidth,
@@ -1592,11 +1594,11 @@ var CellComponent = import_react4.default.memo(({
1592
1594
  }) => {
1593
1595
  const cellRef = import_react4.useCallback((el) => {
1594
1596
  if (el) {
1595
- const rowIndex2 = row - 1;
1596
- const colIndex2 = columnToIndex(col);
1597
+ const rowIndex = row - 1;
1598
+ const colIndex = columnToIndex(col);
1597
1599
  return selectionManager.setupCellElement(el, {
1598
- row: rowIndex2,
1599
- col: colIndex2
1600
+ row: rowIndex,
1601
+ col: colIndex
1600
1602
  });
1601
1603
  }
1602
1604
  }, [row, col, selectionManager]);
@@ -1624,16 +1626,22 @@ var CellComponent = import_react4.default.memo(({
1624
1626
  });
1625
1627
  const inputRef = import_react4.useCallback((el) => {
1626
1628
  if (el) {
1627
- const rowIndex2 = row - 1;
1628
- const colIndex2 = columnToIndex(col);
1629
+ const rowIndex = row - 1;
1630
+ const colIndex = columnToIndex(col);
1629
1631
  return selectionManager.setupInputElement(el, {
1630
- rowIndex: rowIndex2,
1631
- colIndex: colIndex2
1632
+ rowIndex,
1633
+ colIndex
1632
1634
  });
1633
1635
  }
1634
1636
  }, [row, col, selectionManager]);
1635
1637
  return /* @__PURE__ */ jsx_runtime3.jsxs("div", {
1636
1638
  ref: cellRef,
1639
+ id: domId,
1640
+ role: "gridcell",
1641
+ "aria-rowindex": row + 1,
1642
+ "aria-colindex": colIndex + 2,
1643
+ "aria-selected": isSelected,
1644
+ "data-cell-address": `${col}${row}`,
1637
1645
  className: cn("rsp-cell", {
1638
1646
  "rsp-cell-multiline": typeof value === "string" && value.includes(`
1639
1647
  `),
@@ -1653,6 +1661,7 @@ var CellComponent = import_react4.default.memo(({
1653
1661
  children: [
1654
1662
  isBeingEdited ? /* @__PURE__ */ jsx_runtime3.jsx("textarea", {
1655
1663
  className: "rsp-cell-editor",
1664
+ "aria-label": `Edit ${col}${row}`,
1656
1665
  "data-testid": `spreadsheet-cell-input-${col}${row}`,
1657
1666
  ref: inputRef,
1658
1667
  defaultValue: inputValue?.toString() ?? "",
@@ -1731,6 +1740,10 @@ var HeaderComponent = import_react4.default.memo(({
1731
1740
  const content = type === "col" ? reverseHeaders ? index + 1 : indexToColumn(index) : reverseHeaders ? rowToLetter(index + 1) : index + 1;
1732
1741
  return /* @__PURE__ */ jsx_runtime3.jsxs("div", {
1733
1742
  ref: headerRef,
1743
+ role: type === "col" ? "columnheader" : "rowheader",
1744
+ "aria-colindex": type === "col" ? index + 2 : 1,
1745
+ "aria-rowindex": type === "row" ? index + 2 : 1,
1746
+ "aria-selected": isSelected,
1734
1747
  className: cn("rsp-header", `rsp-header-${type}`, {
1735
1748
  "rsp-header-selected": isSelected
1736
1749
  }),
@@ -1875,6 +1888,9 @@ var Spreadsheet = import_react4.forwardRef(({
1875
1888
  style,
1876
1889
  className,
1877
1890
  containerProps,
1891
+ "aria-label": ariaLabel = "Spreadsheet",
1892
+ "aria-labelledby": ariaLabelledBy,
1893
+ "aria-describedby": ariaDescribedBy,
1878
1894
  cellData: controlledCellData,
1879
1895
  onCellDataChange,
1880
1896
  onCellDataChangeError,
@@ -1897,6 +1913,7 @@ var Spreadsheet = import_react4.forwardRef(({
1897
1913
  parentSelected = true,
1898
1914
  disableClipboard = false
1899
1915
  }, ref) => {
1916
+ const gridId = import_react4.useId();
1900
1917
  const [{ width, height }, containerRef] = useResizeObserver();
1901
1918
  const [containerDivRef, setContainerDivRef] = import_react4.useState(null);
1902
1919
  const colorScheme = useColorScheme(containerDivRef);
@@ -2090,15 +2107,15 @@ var Spreadsheet = import_react4.forwardRef(({
2090
2107
  };
2091
2108
  const effectsRef = import_react4.useRef(effectsRefOb);
2092
2109
  effectsRef.current = effectsRefOb;
2093
- const setupCanvas = import_react4.useCallback((canvas, width2, height2) => {
2110
+ const setupCanvas = import_react4.useCallback((canvas, width, height) => {
2094
2111
  const ctx = canvas.getContext("2d");
2095
2112
  if (!ctx)
2096
2113
  return null;
2097
2114
  const devicePixelRatio = window.devicePixelRatio || 1;
2098
- canvas.width = width2 * devicePixelRatio;
2099
- canvas.height = height2 * devicePixelRatio;
2100
- canvas.style.width = width2 + "px";
2101
- canvas.style.height = height2 + "px";
2115
+ canvas.width = width * devicePixelRatio;
2116
+ canvas.height = height * devicePixelRatio;
2117
+ canvas.style.width = width + "px";
2118
+ canvas.style.height = height + "px";
2102
2119
  ctx.scale(devicePixelRatio, devicePixelRatio);
2103
2120
  return ctx;
2104
2121
  }, []);
@@ -2326,36 +2343,36 @@ var Spreadsheet = import_react4.forwardRef(({
2326
2343
  return selectionManager.listenToUpdateData((updates) => {
2327
2344
  const editingState = selectionManager.isEditing;
2328
2345
  try {
2329
- const { cellData: cellData2, onCellDataChange: onCellDataChange2 } = effectsRef.current;
2330
- const newData = new Map(cellData2);
2346
+ const { cellData, onCellDataChange } = effectsRef.current;
2347
+ const newData = new Map(cellData);
2331
2348
  updates.forEach(({ value, rowIndex, colIndex }) => {
2332
2349
  const key = `${indexToColumn(colIndex)}${rowIndex + 1}`;
2333
- const parseValue2 = effectsRef.current.parseValue;
2350
+ const parseValue = effectsRef.current.parseValue;
2334
2351
  if (value === "") {
2335
2352
  newData.delete(key);
2336
2353
  } else {
2337
2354
  let parsedValue = value;
2338
- if (parseValue2 && typeof value === "string") {
2355
+ if (parseValue && typeof value === "string") {
2339
2356
  const match = key.match(/^([A-Z]+)(\d+)$/);
2340
2357
  const col = match?.[1];
2341
2358
  const rowText = match?.[2];
2342
2359
  if (col && rowText) {
2343
2360
  const row = parseInt(rowText, 10);
2344
- const colIndex2 = columnToIndex(col);
2345
- const rowIndex2 = row - 1;
2346
- parsedValue = parseValue2(value, {
2361
+ const colIndex = columnToIndex(col);
2362
+ const rowIndex = row - 1;
2363
+ parsedValue = parseValue(value, {
2347
2364
  id: key,
2348
2365
  row,
2349
2366
  col,
2350
- rowIndex: rowIndex2,
2351
- colIndex: colIndex2
2367
+ rowIndex,
2368
+ colIndex
2352
2369
  });
2353
2370
  }
2354
2371
  }
2355
2372
  newData.set(key, parsedValue);
2356
2373
  }
2357
2374
  });
2358
- onCellDataChange2?.(newData);
2375
+ onCellDataChange?.(newData);
2359
2376
  setInternalCellData(newData);
2360
2377
  } catch (error) {
2361
2378
  console.error("Failed to update spreadsheet cell data:", error);
@@ -2394,6 +2411,12 @@ var Spreadsheet = import_react4.forwardRef(({
2394
2411
  });
2395
2412
  }, [selectionManager, clipboardManager, disableClipboard]);
2396
2413
  const handleWheel = import_react4.useCallback((e) => {
2414
+ const target = e.target;
2415
+ const currentTarget = e.currentTarget;
2416
+ const overlay = target instanceof Element ? target.closest(SPREADSHEET_OVERLAY_SELECTOR) : null;
2417
+ if (overlay && currentTarget instanceof Node && currentTarget.contains(overlay)) {
2418
+ return;
2419
+ }
2397
2420
  e.preventDefault();
2398
2421
  e.stopPropagation();
2399
2422
  setViewport((previous) => ({
@@ -2532,13 +2555,13 @@ var Spreadsheet = import_react4.forwardRef(({
2532
2555
  const handleColumnResizeDoubleClick = import_react4.useCallback((e, col) => {
2533
2556
  e.preventDefault();
2534
2557
  e.stopPropagation();
2535
- const { viewport: viewport2, gridDims: gridDims2, height: height2, cellData: cellData2 } = effectsRef.current;
2558
+ const { viewport, gridDims, height, cellData } = effectsRef.current;
2536
2559
  let maxWidth = 50;
2537
- const startRow = Math.max(0, Math.floor(viewport2.scrollY / gridDims2.cellHeight));
2538
- const endRow = Math.ceil((viewport2.scrollY + height2) / gridDims2.cellHeight) + 1;
2560
+ const startRow = Math.max(0, Math.floor(viewport.scrollY / gridDims.cellHeight));
2561
+ const endRow = Math.ceil((viewport.scrollY + height) / gridDims.cellHeight) + 1;
2539
2562
  for (let row = startRow;row <= endRow; row++) {
2540
2563
  const cellKey = `${col}${getRowNumber(row)}`;
2541
- const content = cellData2.get(cellKey) ?? "";
2564
+ const content = cellData.get(cellKey) ?? "";
2542
2565
  if (content) {
2543
2566
  const estimatedWidth = String(content).length * 8 + 20;
2544
2567
  maxWidth = Math.max(maxWidth, estimatedWidth);
@@ -2807,8 +2830,8 @@ var Spreadsheet = import_react4.forwardRef(({
2807
2830
  return editingCleanup;
2808
2831
  }, [selectionManager]);
2809
2832
  import_react4.default.useEffect(() => {
2810
- return selectionManager.observeStateChange((state) => state.hasFocus, (hasFocus2) => {
2811
- if (hasFocus2 && selectedOverlayId) {
2833
+ return selectionManager.observeStateChange((state) => state.hasFocus, (hasFocus) => {
2834
+ if (hasFocus && selectedOverlayId) {
2812
2835
  setSelectedOverlay(null);
2813
2836
  }
2814
2837
  });
@@ -2911,6 +2934,21 @@ var Spreadsheet = import_react4.forwardRef(({
2911
2934
  const visibleCells = import_react4.default.useMemo(() => getVisibleCells(), [getVisibleCells]);
2912
2935
  const visibleColumnHeaders = import_react4.default.useMemo(() => getVisibleColumnHeaders(), [getVisibleColumnHeaders]);
2913
2936
  const visibleRowHeaders = import_react4.default.useMemo(() => getVisibleRowHeaders(), [getVisibleRowHeaders]);
2937
+ const activeAddress = import_selection_manager.useSelectionManager(selectionManager, () => {
2938
+ const cell = selectionManager.getTopLeftCellInSelection();
2939
+ return cell ? `${indexToColumn(cell.col)}${cell.row + 1}` : undefined;
2940
+ });
2941
+ const activeCellId = visibleCells.some((cell) => `${cell.col}${cell.row}` === activeAddress) ? `${gridId}-cell-${activeAddress}` : undefined;
2942
+ const accessibilityManager = selectionManager;
2943
+ import_react4.useLayoutEffect(() => {
2944
+ accessibilityManager.setInputCaptureAccessibility?.({
2945
+ label: `${ariaLabel} keyboard input`,
2946
+ controls: gridId,
2947
+ activeDescendant: activeCellId,
2948
+ describedBy: ariaDescribedBy
2949
+ });
2950
+ return () => accessibilityManager.setInputCaptureAccessibility?.({});
2951
+ }, [accessibilityManager, ariaLabel, ariaDescribedBy, gridId, activeCellId]);
2914
2952
  const hasFocus = import_selection_manager.useSelectionManager(selectionManager, () => selectionManager.hasFocus);
2915
2953
  const referenceSelection = import_selection_manager.useSelectionManager(selectionManager, () => selectionManager.referenceSelection);
2916
2954
  const referenceSelectionRect = import_react4.useMemo(() => {
@@ -2986,7 +3024,21 @@ var Spreadsheet = import_react4.forwardRef(({
2986
3024
  width,
2987
3025
  height
2988
3026
  },
3027
+ id: gridId,
3028
+ role: "grid",
3029
+ "aria-activedescendant": activeCellId,
3030
+ "aria-label": ariaLabel,
3031
+ "aria-labelledby": ariaLabelledBy,
3032
+ "aria-describedby": ariaDescribedBy,
3033
+ "aria-rowcount": finiteRowCount === undefined ? -1 : finiteRowCount + 1,
3034
+ "aria-colcount": finiteColumnCount === undefined ? -1 : finiteColumnCount + 1,
3035
+ "aria-multiselectable": "true",
2989
3036
  tabIndex: 0,
3037
+ onFocus: (event) => {
3038
+ if (event.target === event.currentTarget && !selectionManager.hasFocus) {
3039
+ selectionManager.focus();
3040
+ }
3041
+ },
2990
3042
  children: [
2991
3043
  /* @__PURE__ */ jsx_runtime3.jsx("span", {
2992
3044
  ref: gridLineProbeRef,
@@ -3002,12 +3054,15 @@ var Spreadsheet = import_react4.forwardRef(({
3002
3054
  }),
3003
3055
  /* @__PURE__ */ jsx_runtime3.jsx("canvas", {
3004
3056
  ref: canvasRef,
3057
+ "aria-hidden": "true",
3005
3058
  className: "rsp-canvas",
3006
3059
  "data-testid": "spreadsheet-canvas",
3007
3060
  style: { width, height }
3008
3061
  }),
3009
3062
  /* @__PURE__ */ jsx_runtime3.jsx("div", {
3010
3063
  className: "rsp-column-headers",
3064
+ role: "row",
3065
+ "aria-rowindex": 1,
3011
3066
  "data-testid": "spreadsheet-column-headers",
3012
3067
  style: {
3013
3068
  left: gridDims.headerWidth,
@@ -3050,16 +3105,21 @@ var Spreadsheet = import_react4.forwardRef(({
3050
3105
  transformOrigin: "0 0"
3051
3106
  },
3052
3107
  children: visibleRowHeaders.map(({ row, rowIndex }) => {
3053
- return /* @__PURE__ */ jsx_runtime3.jsx(HeaderComponent, {
3054
- index: rowIndex,
3055
- type: "row",
3056
- selectionManager,
3057
- getColumnWidth,
3058
- getRowHeight,
3059
- handleColumnResizeStart,
3060
- handleColumnResizeDoubleClick,
3061
- handleRowResizeStart,
3062
- handleRowResizeDoubleClick
3108
+ return /* @__PURE__ */ jsx_runtime3.jsx("div", {
3109
+ role: "row",
3110
+ "aria-rowindex": row + 1,
3111
+ "aria-owns": visibleCells.filter((cell) => cell.row === row).map((cell) => `${gridId}-cell-${cell.col}${cell.row}`).join(" ") || undefined,
3112
+ children: /* @__PURE__ */ jsx_runtime3.jsx(HeaderComponent, {
3113
+ index: rowIndex,
3114
+ type: "row",
3115
+ selectionManager,
3116
+ getColumnWidth,
3117
+ getRowHeight,
3118
+ handleColumnResizeStart,
3119
+ handleColumnResizeDoubleClick,
3120
+ handleRowResizeStart,
3121
+ handleRowResizeDoubleClick
3122
+ })
3063
3123
  }, row);
3064
3124
  })
3065
3125
  })
@@ -3082,6 +3142,7 @@ var Spreadsheet = import_react4.forwardRef(({
3082
3142
  },
3083
3143
  children: [
3084
3144
  visibleCells.map((cell) => /* @__PURE__ */ jsx_runtime3.jsx(CellComponent, {
3145
+ domId: `${gridId}-cell-${cell.col}${cell.row}`,
3085
3146
  value: cellData.get(`${cell.col}${cell.row}`) ?? "",
3086
3147
  row: cell.row,
3087
3148
  col: cell.col,
@@ -3119,6 +3180,7 @@ var Spreadsheet = import_react4.forwardRef(({
3119
3180
  const isTextBox = child.component === "RichText";
3120
3181
  const isMinimized = Boolean(child.minimized && !isTextBox);
3121
3182
  return /* @__PURE__ */ jsx_runtime3.jsxs("div", {
3183
+ "data-rsp-overlay": child.id,
3122
3184
  style: {
3123
3185
  position: "absolute",
3124
3186
  width: child.width,
@@ -3186,7 +3248,7 @@ var Spreadsheet = import_react4.forwardRef(({
3186
3248
  Spreadsheet.displayName = "Spreadsheet";
3187
3249
  // src/workbook/workbook.tsx
3188
3250
  var import_formula_engine2 = require("@ricsam/formula-engine");
3189
- var import_react6 = __toESM(require("react"));
3251
+ var import_react6 = __toESM(require("react"), 1);
3190
3252
 
3191
3253
  // src/utils/use-scheme-root.ts
3192
3254
  var import_react5 = require("react");
@@ -3912,11 +3974,11 @@ function FormulaSheet({
3912
3974
  });
3913
3975
  }
3914
3976
  }),
3915
- spreadsheetSelectionManager.observeStateChange((state2) => {
3916
- const currentSelection = state2.selections.length === 1 ? state2.selections[0] : undefined;
3977
+ spreadsheetSelectionManager.observeStateChange((state) => {
3978
+ const currentSelection = state.selections.length === 1 ? state.selections[0] : undefined;
3917
3979
  return currentSelection;
3918
- }, (selection2) => {
3919
- setSelectedArea(selection2 ?? null);
3980
+ }, (selection) => {
3981
+ setSelectedArea(selection ?? null);
3920
3982
  })
3921
3983
  ];
3922
3984
  const handleUndoRedoKeyDown = (event) => {
@@ -4032,7 +4094,7 @@ function FormulaSheet({
4032
4094
  workbookName
4033
4095
  });
4034
4096
  if (!tableInfo) {
4035
- const style2 = conditionalStyle ? {
4097
+ const style = conditionalStyle ? {
4036
4098
  ...conditionalStyle.backgroundColor && {
4037
4099
  backgroundColor: conditionalStyle.backgroundColor
4038
4100
  },
@@ -4054,7 +4116,7 @@ function FormulaSheet({
4054
4116
  ...getBorderCellStyle(conditionalStyle),
4055
4117
  ...getWrapCellStyle(conditionalStyle)
4056
4118
  } : {};
4057
- return customCellStyle ? customCellStyle(cell, style2) : style2;
4119
+ return customCellStyle ? customCellStyle(cell, style) : style;
4058
4120
  }
4059
4121
  const isHeaderRow = cell.rowIndex === tableInfo.start.rowIndex;
4060
4122
  const isFirstColumn = cell.colIndex === tableInfo.start.colIndex;
@@ -4553,4 +4615,4 @@ var useClickAway = (ref, callback) => {
4553
4615
  }, [ref, callback]);
4554
4616
  };
4555
4617
 
4556
- //# debugId=E584D6343578997564756E2164756E21
4618
+ //# debugId=95BAABDC3CCBBC2364756E2164756E21