@ricsam/react-spreadsheets 0.0.11 → 0.0.13

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
@@ -344,6 +344,32 @@ Wheel events originating inside a floating overlay are left to that overlay, so
344
344
  scrollable editors and other interactive overlay content keep their native
345
345
  scroll behavior.
346
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
+
347
373
  ## Development
348
374
 
349
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");
@@ -1580,6 +1580,7 @@ var CellComponent = import_react4.default.memo(({
1580
1580
  value,
1581
1581
  row,
1582
1582
  col,
1583
+ domId,
1583
1584
  selectionManager,
1584
1585
  cellToPixelBase,
1585
1586
  getColumnWidth,
@@ -1593,11 +1594,11 @@ var CellComponent = import_react4.default.memo(({
1593
1594
  }) => {
1594
1595
  const cellRef = import_react4.useCallback((el) => {
1595
1596
  if (el) {
1596
- const rowIndex2 = row - 1;
1597
- const colIndex2 = columnToIndex(col);
1597
+ const rowIndex = row - 1;
1598
+ const colIndex = columnToIndex(col);
1598
1599
  return selectionManager.setupCellElement(el, {
1599
- row: rowIndex2,
1600
- col: colIndex2
1600
+ row: rowIndex,
1601
+ col: colIndex
1601
1602
  });
1602
1603
  }
1603
1604
  }, [row, col, selectionManager]);
@@ -1625,16 +1626,22 @@ var CellComponent = import_react4.default.memo(({
1625
1626
  });
1626
1627
  const inputRef = import_react4.useCallback((el) => {
1627
1628
  if (el) {
1628
- const rowIndex2 = row - 1;
1629
- const colIndex2 = columnToIndex(col);
1629
+ const rowIndex = row - 1;
1630
+ const colIndex = columnToIndex(col);
1630
1631
  return selectionManager.setupInputElement(el, {
1631
- rowIndex: rowIndex2,
1632
- colIndex: colIndex2
1632
+ rowIndex,
1633
+ colIndex
1633
1634
  });
1634
1635
  }
1635
1636
  }, [row, col, selectionManager]);
1636
1637
  return /* @__PURE__ */ jsx_runtime3.jsxs("div", {
1637
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}`,
1638
1645
  className: cn("rsp-cell", {
1639
1646
  "rsp-cell-multiline": typeof value === "string" && value.includes(`
1640
1647
  `),
@@ -1654,6 +1661,7 @@ var CellComponent = import_react4.default.memo(({
1654
1661
  children: [
1655
1662
  isBeingEdited ? /* @__PURE__ */ jsx_runtime3.jsx("textarea", {
1656
1663
  className: "rsp-cell-editor",
1664
+ "aria-label": `Edit ${col}${row}`,
1657
1665
  "data-testid": `spreadsheet-cell-input-${col}${row}`,
1658
1666
  ref: inputRef,
1659
1667
  defaultValue: inputValue?.toString() ?? "",
@@ -1732,6 +1740,10 @@ var HeaderComponent = import_react4.default.memo(({
1732
1740
  const content = type === "col" ? reverseHeaders ? index + 1 : indexToColumn(index) : reverseHeaders ? rowToLetter(index + 1) : index + 1;
1733
1741
  return /* @__PURE__ */ jsx_runtime3.jsxs("div", {
1734
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,
1735
1747
  className: cn("rsp-header", `rsp-header-${type}`, {
1736
1748
  "rsp-header-selected": isSelected
1737
1749
  }),
@@ -1876,6 +1888,9 @@ var Spreadsheet = import_react4.forwardRef(({
1876
1888
  style,
1877
1889
  className,
1878
1890
  containerProps,
1891
+ "aria-label": ariaLabel = "Spreadsheet",
1892
+ "aria-labelledby": ariaLabelledBy,
1893
+ "aria-describedby": ariaDescribedBy,
1879
1894
  cellData: controlledCellData,
1880
1895
  onCellDataChange,
1881
1896
  onCellDataChangeError,
@@ -1898,6 +1913,7 @@ var Spreadsheet = import_react4.forwardRef(({
1898
1913
  parentSelected = true,
1899
1914
  disableClipboard = false
1900
1915
  }, ref) => {
1916
+ const gridId = import_react4.useId();
1901
1917
  const [{ width, height }, containerRef] = useResizeObserver();
1902
1918
  const [containerDivRef, setContainerDivRef] = import_react4.useState(null);
1903
1919
  const colorScheme = useColorScheme(containerDivRef);
@@ -2091,15 +2107,15 @@ var Spreadsheet = import_react4.forwardRef(({
2091
2107
  };
2092
2108
  const effectsRef = import_react4.useRef(effectsRefOb);
2093
2109
  effectsRef.current = effectsRefOb;
2094
- const setupCanvas = import_react4.useCallback((canvas, width2, height2) => {
2110
+ const setupCanvas = import_react4.useCallback((canvas, width, height) => {
2095
2111
  const ctx = canvas.getContext("2d");
2096
2112
  if (!ctx)
2097
2113
  return null;
2098
2114
  const devicePixelRatio = window.devicePixelRatio || 1;
2099
- canvas.width = width2 * devicePixelRatio;
2100
- canvas.height = height2 * devicePixelRatio;
2101
- canvas.style.width = width2 + "px";
2102
- 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";
2103
2119
  ctx.scale(devicePixelRatio, devicePixelRatio);
2104
2120
  return ctx;
2105
2121
  }, []);
@@ -2327,36 +2343,36 @@ var Spreadsheet = import_react4.forwardRef(({
2327
2343
  return selectionManager.listenToUpdateData((updates) => {
2328
2344
  const editingState = selectionManager.isEditing;
2329
2345
  try {
2330
- const { cellData: cellData2, onCellDataChange: onCellDataChange2 } = effectsRef.current;
2331
- const newData = new Map(cellData2);
2346
+ const { cellData, onCellDataChange } = effectsRef.current;
2347
+ const newData = new Map(cellData);
2332
2348
  updates.forEach(({ value, rowIndex, colIndex }) => {
2333
2349
  const key = `${indexToColumn(colIndex)}${rowIndex + 1}`;
2334
- const parseValue2 = effectsRef.current.parseValue;
2350
+ const parseValue = effectsRef.current.parseValue;
2335
2351
  if (value === "") {
2336
2352
  newData.delete(key);
2337
2353
  } else {
2338
2354
  let parsedValue = value;
2339
- if (parseValue2 && typeof value === "string") {
2355
+ if (parseValue && typeof value === "string") {
2340
2356
  const match = key.match(/^([A-Z]+)(\d+)$/);
2341
2357
  const col = match?.[1];
2342
2358
  const rowText = match?.[2];
2343
2359
  if (col && rowText) {
2344
2360
  const row = parseInt(rowText, 10);
2345
- const colIndex2 = columnToIndex(col);
2346
- const rowIndex2 = row - 1;
2347
- parsedValue = parseValue2(value, {
2361
+ const colIndex = columnToIndex(col);
2362
+ const rowIndex = row - 1;
2363
+ parsedValue = parseValue(value, {
2348
2364
  id: key,
2349
2365
  row,
2350
2366
  col,
2351
- rowIndex: rowIndex2,
2352
- colIndex: colIndex2
2367
+ rowIndex,
2368
+ colIndex
2353
2369
  });
2354
2370
  }
2355
2371
  }
2356
2372
  newData.set(key, parsedValue);
2357
2373
  }
2358
2374
  });
2359
- onCellDataChange2?.(newData);
2375
+ onCellDataChange?.(newData);
2360
2376
  setInternalCellData(newData);
2361
2377
  } catch (error) {
2362
2378
  console.error("Failed to update spreadsheet cell data:", error);
@@ -2539,13 +2555,13 @@ var Spreadsheet = import_react4.forwardRef(({
2539
2555
  const handleColumnResizeDoubleClick = import_react4.useCallback((e, col) => {
2540
2556
  e.preventDefault();
2541
2557
  e.stopPropagation();
2542
- const { viewport: viewport2, gridDims: gridDims2, height: height2, cellData: cellData2 } = effectsRef.current;
2558
+ const { viewport, gridDims, height, cellData } = effectsRef.current;
2543
2559
  let maxWidth = 50;
2544
- const startRow = Math.max(0, Math.floor(viewport2.scrollY / gridDims2.cellHeight));
2545
- 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;
2546
2562
  for (let row = startRow;row <= endRow; row++) {
2547
2563
  const cellKey = `${col}${getRowNumber(row)}`;
2548
- const content = cellData2.get(cellKey) ?? "";
2564
+ const content = cellData.get(cellKey) ?? "";
2549
2565
  if (content) {
2550
2566
  const estimatedWidth = String(content).length * 8 + 20;
2551
2567
  maxWidth = Math.max(maxWidth, estimatedWidth);
@@ -2814,8 +2830,8 @@ var Spreadsheet = import_react4.forwardRef(({
2814
2830
  return editingCleanup;
2815
2831
  }, [selectionManager]);
2816
2832
  import_react4.default.useEffect(() => {
2817
- return selectionManager.observeStateChange((state) => state.hasFocus, (hasFocus2) => {
2818
- if (hasFocus2 && selectedOverlayId) {
2833
+ return selectionManager.observeStateChange((state) => state.hasFocus, (hasFocus) => {
2834
+ if (hasFocus && selectedOverlayId) {
2819
2835
  setSelectedOverlay(null);
2820
2836
  }
2821
2837
  });
@@ -2918,6 +2934,21 @@ var Spreadsheet = import_react4.forwardRef(({
2918
2934
  const visibleCells = import_react4.default.useMemo(() => getVisibleCells(), [getVisibleCells]);
2919
2935
  const visibleColumnHeaders = import_react4.default.useMemo(() => getVisibleColumnHeaders(), [getVisibleColumnHeaders]);
2920
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]);
2921
2952
  const hasFocus = import_selection_manager.useSelectionManager(selectionManager, () => selectionManager.hasFocus);
2922
2953
  const referenceSelection = import_selection_manager.useSelectionManager(selectionManager, () => selectionManager.referenceSelection);
2923
2954
  const referenceSelectionRect = import_react4.useMemo(() => {
@@ -2993,7 +3024,21 @@ var Spreadsheet = import_react4.forwardRef(({
2993
3024
  width,
2994
3025
  height
2995
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",
2996
3036
  tabIndex: 0,
3037
+ onFocus: (event) => {
3038
+ if (event.target === event.currentTarget && !selectionManager.hasFocus) {
3039
+ selectionManager.focus();
3040
+ }
3041
+ },
2997
3042
  children: [
2998
3043
  /* @__PURE__ */ jsx_runtime3.jsx("span", {
2999
3044
  ref: gridLineProbeRef,
@@ -3009,12 +3054,15 @@ var Spreadsheet = import_react4.forwardRef(({
3009
3054
  }),
3010
3055
  /* @__PURE__ */ jsx_runtime3.jsx("canvas", {
3011
3056
  ref: canvasRef,
3057
+ "aria-hidden": "true",
3012
3058
  className: "rsp-canvas",
3013
3059
  "data-testid": "spreadsheet-canvas",
3014
3060
  style: { width, height }
3015
3061
  }),
3016
3062
  /* @__PURE__ */ jsx_runtime3.jsx("div", {
3017
3063
  className: "rsp-column-headers",
3064
+ role: "row",
3065
+ "aria-rowindex": 1,
3018
3066
  "data-testid": "spreadsheet-column-headers",
3019
3067
  style: {
3020
3068
  left: gridDims.headerWidth,
@@ -3057,16 +3105,21 @@ var Spreadsheet = import_react4.forwardRef(({
3057
3105
  transformOrigin: "0 0"
3058
3106
  },
3059
3107
  children: visibleRowHeaders.map(({ row, rowIndex }) => {
3060
- return /* @__PURE__ */ jsx_runtime3.jsx(HeaderComponent, {
3061
- index: rowIndex,
3062
- type: "row",
3063
- selectionManager,
3064
- getColumnWidth,
3065
- getRowHeight,
3066
- handleColumnResizeStart,
3067
- handleColumnResizeDoubleClick,
3068
- handleRowResizeStart,
3069
- 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
+ })
3070
3123
  }, row);
3071
3124
  })
3072
3125
  })
@@ -3089,6 +3142,7 @@ var Spreadsheet = import_react4.forwardRef(({
3089
3142
  },
3090
3143
  children: [
3091
3144
  visibleCells.map((cell) => /* @__PURE__ */ jsx_runtime3.jsx(CellComponent, {
3145
+ domId: `${gridId}-cell-${cell.col}${cell.row}`,
3092
3146
  value: cellData.get(`${cell.col}${cell.row}`) ?? "",
3093
3147
  row: cell.row,
3094
3148
  col: cell.col,
@@ -3194,7 +3248,7 @@ var Spreadsheet = import_react4.forwardRef(({
3194
3248
  Spreadsheet.displayName = "Spreadsheet";
3195
3249
  // src/workbook/workbook.tsx
3196
3250
  var import_formula_engine2 = require("@ricsam/formula-engine");
3197
- var import_react6 = __toESM(require("react"));
3251
+ var import_react6 = __toESM(require("react"), 1);
3198
3252
 
3199
3253
  // src/utils/use-scheme-root.ts
3200
3254
  var import_react5 = require("react");
@@ -3419,13 +3473,21 @@ class WorkbookSelectionManager {
3419
3473
  if (cachedSelections && manager.getState().selections.length === 0) {
3420
3474
  manager.replaceSelections(cachedSelections);
3421
3475
  }
3476
+ let previousFocus = manager.getState().hasFocus;
3477
+ let previousSelections = JSON.stringify(manager.getState().selections);
3422
3478
  const focusCleanup = manager.observeStateChange((state) => state.hasFocus, (hasFocus) => {
3479
+ if (hasFocus === previousFocus)
3480
+ return;
3481
+ previousFocus = hasFocus;
3423
3482
  if (hasFocus && !this.applyingRangeFocus.has(manager)) {
3424
3483
  this.rememberSelections(manager, registeredContext);
3425
3484
  this.emitChange();
3426
3485
  }
3427
3486
  });
3428
3487
  const selectionCleanup = manager.observeStateChange((state) => JSON.stringify(state.selections), (selectionsJson) => {
3488
+ if (selectionsJson === previousSelections)
3489
+ return;
3490
+ previousSelections = selectionsJson;
3429
3491
  if (this.applyingRangeFocus.has(manager))
3430
3492
  return;
3431
3493
  const selections = JSON.parse(selectionsJson);
@@ -3920,11 +3982,11 @@ function FormulaSheet({
3920
3982
  });
3921
3983
  }
3922
3984
  }),
3923
- spreadsheetSelectionManager.observeStateChange((state2) => {
3924
- const currentSelection = state2.selections.length === 1 ? state2.selections[0] : undefined;
3985
+ spreadsheetSelectionManager.observeStateChange((state) => {
3986
+ const currentSelection = state.selections.length === 1 ? state.selections[0] : undefined;
3925
3987
  return currentSelection;
3926
- }, (selection2) => {
3927
- setSelectedArea(selection2 ?? null);
3988
+ }, (selection) => {
3989
+ setSelectedArea(selection ?? null);
3928
3990
  })
3929
3991
  ];
3930
3992
  const handleUndoRedoKeyDown = (event) => {
@@ -4040,7 +4102,7 @@ function FormulaSheet({
4040
4102
  workbookName
4041
4103
  });
4042
4104
  if (!tableInfo) {
4043
- const style2 = conditionalStyle ? {
4105
+ const style = conditionalStyle ? {
4044
4106
  ...conditionalStyle.backgroundColor && {
4045
4107
  backgroundColor: conditionalStyle.backgroundColor
4046
4108
  },
@@ -4062,7 +4124,7 @@ function FormulaSheet({
4062
4124
  ...getBorderCellStyle(conditionalStyle),
4063
4125
  ...getWrapCellStyle(conditionalStyle)
4064
4126
  } : {};
4065
- return customCellStyle ? customCellStyle(cell, style2) : style2;
4127
+ return customCellStyle ? customCellStyle(cell, style) : style;
4066
4128
  }
4067
4129
  const isHeaderRow = cell.rowIndex === tableInfo.start.rowIndex;
4068
4130
  const isFirstColumn = cell.colIndex === tableInfo.start.colIndex;
@@ -4561,4 +4623,4 @@ var useClickAway = (ref, callback) => {
4561
4623
  }, [ref, callback]);
4562
4624
  };
4563
4625
 
4564
- //# debugId=64201658718498BD64756E2164756E21
4626
+ //# debugId=422BCC484554257364756E2164756E21