@ricsam/react-spreadsheets 0.0.8 → 0.0.9

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
@@ -134,9 +134,23 @@ function ReferenceAwareSheet({ highlight }: { highlight?: Highlight }) {
134
134
 
135
135
  Update `highlight` from the formula editor's caret/reference analysis. Clearing
136
136
  it removes the decoration, while the selection manager continues to own mouse,
137
- keyboard and edit selection state independently. For `FormulaWorkbook`, pass a
138
- controlled `activeSheet` if the editor should reveal a reference on another
139
- sheet; the same `customCellStyle` prop is forwarded to its active sheet.
137
+ keyboard and edit selection state independently. The same `customCellStyle`
138
+ prop is forwarded to a `FormulaWorkbook`'s active sheet.
139
+
140
+ To focus and reveal the reference itself, share a `WorkbookSelectionManager`
141
+ with the formula sheets and pass the resolved `RangeAddress` to `focusRange`:
142
+
143
+ ```tsx
144
+ const workbookSelection = new WorkbookSelectionManager();
145
+
146
+ workbookSelection.focusRange(referenceAddress, { align: "nearest" });
147
+ ```
148
+
149
+ The call selects the complete target range, focuses its grid and asks the grid
150
+ to reveal it. It returns `true` when the target sheet is already mounted. If a
151
+ controlled `FormulaWorkbook` still needs to switch `activeSheet`, it returns
152
+ `false` and queues the latest request; mounting that sheet applies the focus and
153
+ reveal automatically.
140
154
 
141
155
  For the range currently being inserted into a formula, use selection-manager's
142
156
  reference-picking mode instead. It preserves the primary/editing selection and
@@ -200,6 +214,11 @@ The imperative `SpreadsheetRef.scrollToCell({ row, col }, { align })` reveals a
200
214
  cell without changing selection. Alignment is `"nearest"` by default, with
201
215
  `"start"` and `"end"` available for explicit positioning.
202
216
 
217
+ `SpreadsheetRef.scrollToRange(range, { align })` does the same for an `SMArea`.
218
+ Finite ranges may be supplied in either direction and are clamped to finite grid
219
+ bounds. An open-ended axis reaches the final row or column of a finite grid; on
220
+ an unbounded grid its finite start is used as the reveal anchor.
221
+
203
222
  ## Theming
204
223
 
205
224
  All colors, fonts and sizes are CSS custom properties. The package only ever
package/dist/cjs/lib.cjs CHANGED
@@ -1472,12 +1472,37 @@ var alignScrollOffset = (current, itemStart, itemEnd, viewportSize, align) => {
1472
1472
  return itemStart;
1473
1473
  if (align === "end")
1474
1474
  return itemEnd - viewportSize;
1475
+ if (itemEnd - itemStart > viewportSize) {
1476
+ if (itemEnd <= current)
1477
+ return itemEnd - viewportSize;
1478
+ if (itemStart >= current + viewportSize)
1479
+ return itemStart;
1480
+ return current;
1481
+ }
1475
1482
  if (itemStart < current)
1476
1483
  return itemStart;
1477
1484
  if (itemEnd > current + viewportSize)
1478
1485
  return itemEnd - viewportSize;
1479
1486
  return current;
1480
1487
  };
1488
+ var normalizeScrollRangeAxis = (start, end, finiteCount) => {
1489
+ if (!Number.isFinite(start) || end.type === "number" && !Number.isFinite(end.value)) {
1490
+ return;
1491
+ }
1492
+ const normalizedStart = Math.max(0, Math.floor(start));
1493
+ const normalizedEnd = end.type === "infinity" ? finiteCount === undefined ? normalizedStart : Math.max(0, finiteCount - 1) : Math.max(0, Math.floor(end.value));
1494
+ const lower = Math.min(normalizedStart, normalizedEnd);
1495
+ const upper = Math.max(normalizedStart, normalizedEnd);
1496
+ if (finiteCount === undefined)
1497
+ return { start: lower, end: upper };
1498
+ if (finiteCount === 0)
1499
+ return;
1500
+ const lastIndex = finiteCount - 1;
1501
+ return {
1502
+ start: Math.min(lower, lastIndex),
1503
+ end: Math.min(upper, lastIndex)
1504
+ };
1505
+ };
1481
1506
  var hasCellData = (value) => value !== undefined && value !== "";
1482
1507
  var getSparseUsedRange = (cellData, rowCount, columnCount) => {
1483
1508
  let minRow = Infinity;
@@ -2234,22 +2259,20 @@ var Spreadsheet = import_react4.forwardRef(({
2234
2259
  scrollY: Math.min(Math.max(0, scrollY), maxScrollY)
2235
2260
  };
2236
2261
  }, [columnWidths, finiteColumnCount, finiteRowCount, height, rowHeights, width]);
2237
- const revealCell = import_react4.useCallback((cell, options = {}) => {
2262
+ const revealRange = import_react4.useCallback((range, options = {}) => {
2238
2263
  if (finiteRowCount === 0 || finiteColumnCount === 0)
2239
2264
  return;
2240
- if (!Number.isFinite(cell.row) || !Number.isFinite(cell.col))
2265
+ const rows = normalizeScrollRangeAxis(range.start.row, range.end.row, finiteRowCount);
2266
+ const columns = normalizeScrollRangeAxis(range.start.col, range.end.col, finiteColumnCount);
2267
+ if (!rows || !columns)
2241
2268
  return;
2242
- const requestedRow = Math.max(0, Math.floor(cell.row));
2243
- const requestedCol = Math.max(0, Math.floor(cell.col));
2244
- const row = finiteRowCount === undefined ? requestedRow : Math.min(requestedRow, finiteRowCount - 1);
2245
- const col = finiteColumnCount === undefined ? requestedCol : Math.min(requestedCol, finiteColumnCount - 1);
2246
2269
  setViewport((previous) => {
2247
2270
  const bodyWidth = Math.max(0, width - HEADER_WIDTH * previous.zoom);
2248
2271
  const bodyHeight = Math.max(0, height - HEADER_HEIGHT * previous.zoom);
2249
- const left = getColumnEdgePosition(col, columnWidths) * previous.zoom;
2250
- const right = getColumnEdgePosition(col + 1, columnWidths) * previous.zoom;
2251
- const top = getRowEdgePosition(row, rowHeights) * previous.zoom;
2252
- const bottom = getRowEdgePosition(row + 1, rowHeights) * previous.zoom;
2272
+ const left = getColumnEdgePosition(columns.start, columnWidths) * previous.zoom;
2273
+ const right = getColumnEdgePosition(columns.end + 1, columnWidths) * previous.zoom;
2274
+ const top = getRowEdgePosition(rows.start, rowHeights) * previous.zoom;
2275
+ const bottom = getRowEdgePosition(rows.end + 1, rowHeights) * previous.zoom;
2253
2276
  const desired = clampViewportOffsets(alignScrollOffset(previous.scrollX, left, right, bodyWidth, options.columnAlign ?? "nearest"), alignScrollOffset(previous.scrollY, top, bottom, bodyHeight, options.rowAlign ?? "nearest"), previous.zoom);
2254
2277
  if (desired.scrollX === previous.scrollX && desired.scrollY === previous.scrollY) {
2255
2278
  return previous;
@@ -2265,17 +2288,34 @@ var Spreadsheet = import_react4.forwardRef(({
2265
2288
  rowHeights,
2266
2289
  width
2267
2290
  ]);
2291
+ const revealCell = import_react4.useCallback((cell, options = {}) => {
2292
+ revealRange({
2293
+ start: { row: cell.row, col: cell.col },
2294
+ end: {
2295
+ row: { type: "number", value: cell.row },
2296
+ col: { type: "number", value: cell.col }
2297
+ }
2298
+ }, options);
2299
+ }, [revealRange]);
2268
2300
  const scrollToCell = import_react4.useCallback((cell, options = {}) => {
2269
2301
  const align = options.align ?? "nearest";
2270
2302
  revealCell(cell, { rowAlign: align, columnAlign: align });
2271
2303
  }, [revealCell]);
2304
+ const scrollToRange = import_react4.useCallback((range, options = {}) => {
2305
+ const align = options.align ?? "nearest";
2306
+ revealRange(range, { rowAlign: align, columnAlign: align });
2307
+ }, [revealRange]);
2272
2308
  import_react4.useEffect(() => selectionManager.listenToViewportRequest((request) => {
2309
+ if (request.type === "reveal-range") {
2310
+ scrollToRange(request.range, { align: request.align });
2311
+ return;
2312
+ }
2273
2313
  const isVertical = request.direction === "up" || request.direction === "down";
2274
2314
  revealCell(request.cell, {
2275
2315
  rowAlign: isVertical ? request.align : "nearest",
2276
2316
  columnAlign: isVertical ? "nearest" : request.align
2277
2317
  });
2278
- }), [revealCell, selectionManager]);
2318
+ }), [revealCell, scrollToRange, selectionManager]);
2279
2319
  import_react4.useEffect(() => {
2280
2320
  setViewport((previous) => {
2281
2321
  const next = clampViewportOffsets(previous.scrollX, previous.scrollY, previous.zoom);
@@ -2910,7 +2950,7 @@ var Spreadsheet = import_react4.forwardRef(({
2910
2950
  visibleCells
2911
2951
  ]);
2912
2952
  import_react4.default.useEffect(() => {
2913
- if (!containerDivRef || !hasFocus || !parentSelected) {
2953
+ if (!containerDivRef || !parentSelected) {
2914
2954
  return;
2915
2955
  }
2916
2956
  containerDivRef.addEventListener("wheel", handleWheel, {
@@ -2919,7 +2959,7 @@ var Spreadsheet = import_react4.forwardRef(({
2919
2959
  return () => {
2920
2960
  containerDivRef.removeEventListener("wheel", handleWheel);
2921
2961
  };
2922
- }, [containerDivRef, handleWheel, hasFocus, parentSelected]);
2962
+ }, [containerDivRef, handleWheel, parentSelected]);
2923
2963
  import_react4.useImperativeHandle(ref, () => ({
2924
2964
  focus: () => {
2925
2965
  selectionManager.focus();
@@ -2927,8 +2967,9 @@ var Spreadsheet = import_react4.forwardRef(({
2927
2967
  blur: () => {
2928
2968
  selectionManager.blur();
2929
2969
  },
2930
- scrollToCell
2931
- }), [scrollToCell, selectionManager]);
2970
+ scrollToCell,
2971
+ scrollToRange
2972
+ }), [scrollToCell, scrollToRange, selectionManager]);
2932
2973
  return /* @__PURE__ */ jsx_runtime3.jsx("div", {
2933
2974
  className: cn("rsp-root", className),
2934
2975
  "data-rsp-scheme": colorScheme,
@@ -3273,6 +3314,7 @@ class WorkbookSelectionManager {
3273
3314
  lastFocusedManager = null;
3274
3315
  listeners = new Set;
3275
3316
  cleanupFunctions = new Map;
3317
+ pendingRangeFocus = null;
3276
3318
  smAreaToSpreadsheetRange(area) {
3277
3319
  const rowEnd = area.end.row.type === "infinity" ? { type: "infinity", sign: "positive" } : { type: "number", value: area.end.row.value };
3278
3320
  const colEnd = area.end.col.type === "infinity" ? { type: "infinity", sign: "positive" } : { type: "number", value: area.end.col.value };
@@ -3287,6 +3329,47 @@ class WorkbookSelectionManager {
3287
3329
  }
3288
3330
  };
3289
3331
  }
3332
+ spreadsheetRangeToSmArea(range) {
3333
+ return {
3334
+ start: { row: range.start.row, col: range.start.col },
3335
+ end: {
3336
+ row: range.end.row.type === "infinity" ? { type: "infinity" } : { type: "number", value: range.end.row.value },
3337
+ col: range.end.col.type === "infinity" ? { type: "infinity" } : { type: "number", value: range.end.col.value }
3338
+ }
3339
+ };
3340
+ }
3341
+ cloneRangeAddress(address) {
3342
+ return {
3343
+ workbookName: address.workbookName,
3344
+ sheetName: address.sheetName,
3345
+ range: this.smAreaToSpreadsheetRange(this.spreadsheetRangeToSmArea(address.range))
3346
+ };
3347
+ }
3348
+ matchesContext(address, context) {
3349
+ return address.workbookName === context.workbookName && address.sheetName === context.sheetName;
3350
+ }
3351
+ applyRangeFocus(manager, context, request) {
3352
+ const selection = this.spreadsheetRangeToSmArea(request.address.range);
3353
+ manager.setState({ selections: [selection] });
3354
+ manager.focus();
3355
+ manager.revealRange(selection, request.options);
3356
+ this.lastFocusedManager = { sm: manager, context };
3357
+ this.emitChange();
3358
+ }
3359
+ applyPendingRangeFocusOnMount(manager, context) {
3360
+ const pending = this.pendingRangeFocus;
3361
+ if (!pending || !this.matchesContext(pending.address, context))
3362
+ return;
3363
+ queueMicrotask(() => {
3364
+ if (this.pendingRangeFocus !== pending)
3365
+ return;
3366
+ const mountedContext = this.managers.get(manager);
3367
+ if (!mountedContext || !this.matchesContext(pending.address, mountedContext))
3368
+ return;
3369
+ this.pendingRangeFocus = null;
3370
+ this.applyRangeFocus(manager, mountedContext, pending);
3371
+ });
3372
+ }
3290
3373
  add(manager, context) {
3291
3374
  this.managers.set(manager, context);
3292
3375
  const focusCleanup = manager.observeStateChange((state) => state.hasFocus, (hasFocus) => {
@@ -3312,6 +3395,7 @@ class WorkbookSelectionManager {
3312
3395
  }
3313
3396
  };
3314
3397
  this.cleanupFunctions.set(manager, cleanup);
3398
+ this.applyPendingRangeFocusOnMount(manager, context);
3315
3399
  return cleanup;
3316
3400
  }
3317
3401
  getLastFocusedSelectionManager() {
@@ -3405,22 +3489,31 @@ class WorkbookSelectionManager {
3405
3489
  return false;
3406
3490
  }
3407
3491
  focusCell(workbookName, sheetName, row, col) {
3408
- for (const [manager, context] of this.managers) {
3409
- if (context.workbookName === workbookName && context.sheetName === sheetName) {
3410
- const selection = {
3411
- start: { row, col },
3412
- end: {
3413
- row: { type: "number", value: row },
3414
- col: { type: "number", value: col }
3415
- }
3416
- };
3417
- manager.setState({ selections: [selection] });
3418
- manager.focus();
3419
- this.lastFocusedManager = { sm: manager, context };
3420
- this.emitChange();
3421
- return true;
3492
+ return this.focusRange({
3493
+ workbookName,
3494
+ sheetName,
3495
+ range: {
3496
+ start: { row, col },
3497
+ end: {
3498
+ row: { type: "number", value: row },
3499
+ col: { type: "number", value: col }
3500
+ }
3422
3501
  }
3502
+ });
3503
+ }
3504
+ focusRange(address, options = {}) {
3505
+ const request = {
3506
+ address: this.cloneRangeAddress(address),
3507
+ options: { ...options }
3508
+ };
3509
+ for (const [manager, context] of this.managers) {
3510
+ if (!this.matchesContext(request.address, context))
3511
+ continue;
3512
+ this.pendingRangeFocus = null;
3513
+ this.applyRangeFocus(manager, context, request);
3514
+ return true;
3423
3515
  }
3516
+ this.pendingRangeFocus = request;
3424
3517
  return false;
3425
3518
  }
3426
3519
  emitChange() {
@@ -4417,4 +4510,4 @@ var useClickAway = (ref, callback) => {
4417
4510
  }, [ref, callback]);
4418
4511
  };
4419
4512
 
4420
- //# debugId=5EB66D4C826F4D2464756E2164756E21
4513
+ //# debugId=81701030D90BCDFB64756E2164756E21