@svgrid/grid 3.0.2 → 3.0.4
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 +2 -1
- package/dist/GridMenus.svelte +44 -2
- package/dist/SvChartMenu.svelte +69 -0
- package/dist/SvChartMenu.svelte.d.ts +11 -0
- package/dist/SvChartPanes.svelte +113 -0
- package/dist/SvChartPanes.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.d.ts +110 -39
- package/dist/SvGrid.controller.svelte.js +655 -200
- package/dist/SvGrid.css +105 -2
- package/dist/SvGrid.svelte +386 -514
- package/dist/SvGrid.types.d.ts +210 -9
- package/dist/SvGridCellEditor.svelte +17 -0
- package/dist/SvGridChart.svelte +2257 -329
- package/dist/SvGridChart.svelte.d.ts +6 -106
- package/dist/SvGridChart.types.d.ts +338 -0
- package/dist/SvGridChart.types.js +1 -0
- package/dist/SvGridChartBuilder.svelte +323 -0
- package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
- package/dist/SvGridChartPanel.svelte +221 -275
- package/dist/SvGridChartPickers.svelte +417 -0
- package/dist/SvGridChartPickers.svelte.d.ts +70 -0
- package/dist/SvGridChartView.svelte +13 -14
- package/dist/SvModal.svelte +7 -1
- package/dist/SvModal.svelte.d.ts +5 -0
- package/dist/ai.d.ts +40 -8
- package/dist/ai.js +135 -24
- package/dist/aria.d.ts +11 -0
- package/dist/build-api.js +108 -22
- package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
- package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
- package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
- package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
- package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
- package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
- package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
- package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
- package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
- package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
- package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
- package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
- package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
- package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
- package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
- package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
- package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
- package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
- package/dist/cdn/SvModal-CIZWCcad.js +397 -0
- package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
- package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
- package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
- package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
- package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
- package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
- package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
- package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
- package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
- package/dist/cdn/dismissable-DAHetSNk.js +44 -0
- package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
- package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
- package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
- package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
- package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
- package/dist/cdn/svgrid.js +26 -15
- package/dist/cdn/svgrid.svelte-external.js +26 -15
- package/dist/chart-axes.d.ts +53 -0
- package/dist/chart-axes.js +351 -0
- package/dist/chart-cartesian.d.ts +88 -0
- package/dist/chart-cartesian.js +1862 -0
- package/dist/chart-decimate.d.ts +51 -0
- package/dist/chart-decimate.js +199 -0
- package/dist/chart-export-pdf.d.ts +47 -0
- package/dist/chart-export-pdf.js +187 -0
- package/dist/chart-export.d.ts +2 -1
- package/dist/chart-export.js +18 -3
- package/dist/chart-financial.d.ts +91 -0
- package/dist/chart-financial.js +175 -0
- package/dist/chart-flow.d.ts +10 -0
- package/dist/chart-flow.js +298 -0
- package/dist/chart-format.d.ts +18 -0
- package/dist/chart-format.js +65 -0
- package/dist/chart-grid.d.ts +8 -0
- package/dist/chart-grid.js +190 -0
- package/dist/chart-hierarchy.d.ts +17 -0
- package/dist/chart-hierarchy.js +146 -0
- package/dist/chart-indicators.d.ts +115 -0
- package/dist/chart-indicators.js +389 -0
- package/dist/chart-messages.d.ts +95 -0
- package/dist/chart-messages.js +92 -0
- package/dist/chart-motion.d.ts +16 -0
- package/dist/chart-motion.js +141 -0
- package/dist/chart-panel-messages.d.ts +265 -0
- package/dist/chart-panel-messages.js +285 -0
- package/dist/chart-pivot.d.ts +87 -0
- package/dist/chart-pivot.js +156 -0
- package/dist/chart-polar.d.ts +25 -0
- package/dist/chart-polar.js +700 -0
- package/dist/chart-samples.d.ts +16 -0
- package/dist/chart-samples.js +153 -0
- package/dist/chart-scale.d.ts +113 -0
- package/dist/chart-scale.js +405 -0
- package/dist/chart-stats.d.ts +111 -0
- package/dist/chart-stats.js +419 -0
- package/dist/chart-stream.d.ts +45 -0
- package/dist/chart-stream.js +91 -0
- package/dist/chart-summary.d.ts +27 -0
- package/dist/chart-summary.js +205 -0
- package/dist/chart-sync.svelte.d.ts +32 -0
- package/dist/chart-sync.svelte.js +23 -0
- package/dist/chart-table.d.ts +35 -0
- package/dist/chart-table.js +165 -0
- package/dist/chart-trend.d.ts +19 -0
- package/dist/chart-trend.js +61 -0
- package/dist/chart-types.d.ts +1456 -0
- package/dist/chart-types.js +6 -0
- package/dist/chart-validate.d.ts +43 -0
- package/dist/chart-validate.js +272 -0
- package/dist/chart-zoom.d.ts +40 -0
- package/dist/chart-zoom.js +145 -0
- package/dist/chart.d.ts +186 -865
- package/dist/chart.js +460 -2204
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +137 -18
- package/dist/column-resize.d.ts +3 -0
- package/dist/column-resize.js +4 -1
- package/dist/columns.js +3 -0
- package/dist/command-context.d.ts +19 -0
- package/dist/command-context.js +116 -0
- package/dist/core.d.ts +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +262 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/grid-messages.d.ts +10 -3
- package/dist/grid-messages.js +5 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.js +20 -2
- package/dist/keyboard-handlers.js +88 -31
- package/dist/keyboard.d.ts +55 -0
- package/dist/keyboard.js +132 -0
- package/dist/menus.d.ts +3 -1
- package/dist/menus.js +7 -2
- package/dist/merges.d.ts +97 -0
- package/dist/merges.js +147 -0
- package/dist/row-resize.d.ts +3 -0
- package/dist/row-resize.js +14 -0
- package/dist/scroll-sync.d.ts +1 -0
- package/dist/scroll-sync.js +24 -0
- package/dist/selection.d.ts +16 -0
- package/dist/selection.js +108 -16
- package/dist/shortcut-registry.d.ts +145 -0
- package/dist/shortcut-registry.js +44 -0
- package/dist/svgrid-wrapper.types.d.ts +213 -22
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +6 -1
- package/src/GridMenus.svelte +44 -2
- package/src/SvChartMenu.svelte +69 -0
- package/src/SvChartPanes.svelte +113 -0
- package/src/SvChartPanes.test.ts +66 -0
- package/src/SvGrid.controller.svelte.ts +629 -172
- package/src/SvGrid.css +105 -2
- package/src/SvGrid.svelte +386 -514
- package/src/SvGrid.types.ts +188 -8
- package/src/SvGridCellEditor.svelte +17 -0
- package/src/SvGridChart.svelte +2257 -329
- package/src/SvGridChart.test.ts +1419 -1
- package/src/SvGridChart.types.ts +336 -0
- package/src/SvGridChartBuilder.svelte +323 -0
- package/src/SvGridChartPanel.svelte +221 -275
- package/src/SvGridChartPickers.svelte +417 -0
- package/src/SvGridChartView.svelte +13 -14
- package/src/SvModal.svelte +7 -1
- package/src/SvModal.test.ts +9 -0
- package/src/ai.test.ts +57 -2
- package/src/ai.ts +166 -22
- package/src/aria.d.ts +11 -0
- package/src/build-api.coverage.test.ts +184 -0
- package/src/build-api.ts +107 -18
- package/src/chart-axes.test.ts +573 -0
- package/src/chart-axes.ts +342 -0
- package/src/chart-cartesian.ts +1863 -0
- package/src/chart-decimate.test.ts +249 -0
- package/src/chart-decimate.ts +186 -0
- package/src/chart-export-pdf.test.ts +91 -0
- package/src/chart-export-pdf.ts +207 -0
- package/src/chart-export.test.ts +14 -0
- package/src/chart-export.ts +13 -3
- package/src/chart-financial.ts +208 -0
- package/src/chart-flow.ts +286 -0
- package/src/chart-format.ts +67 -0
- package/src/chart-grid.ts +179 -0
- package/src/chart-hierarchy.ts +135 -0
- package/src/chart-indicators.test.ts +443 -0
- package/src/chart-indicators.ts +384 -0
- package/src/chart-messages.test.ts +21 -0
- package/src/chart-messages.ts +184 -0
- package/src/chart-motion.test.ts +71 -0
- package/src/chart-motion.ts +148 -0
- package/src/chart-panel-messages.test.ts +29 -0
- package/src/chart-panel-messages.ts +550 -0
- package/src/chart-pivot.test.ts +54 -0
- package/src/chart-pivot.ts +225 -0
- package/src/chart-polar.ts +674 -0
- package/src/chart-samples.ts +106 -0
- package/src/chart-scale.ts +394 -0
- package/src/chart-series-types.test.ts +714 -0
- package/src/chart-stats.ts +409 -0
- package/src/chart-stream.test.ts +75 -0
- package/src/chart-stream.ts +114 -0
- package/src/chart-summary.test.ts +90 -0
- package/src/chart-summary.ts +197 -0
- package/src/chart-sync.svelte.ts +41 -0
- package/src/chart-table.test.ts +76 -0
- package/src/chart-table.ts +178 -0
- package/src/chart-trend.ts +47 -0
- package/src/chart-types.ts +1309 -0
- package/src/chart-validate.test.ts +100 -0
- package/src/chart-validate.ts +247 -0
- package/src/chart-zoom.test.ts +137 -0
- package/src/chart-zoom.ts +124 -0
- package/src/chart.coverage.test.ts +81 -0
- package/src/chart.test.ts +56 -3
- package/src/chart.ts +557 -2784
- package/src/clipboard.test.ts +151 -0
- package/src/clipboard.ts +145 -16
- package/src/column-resize.ts +7 -1
- package/src/columns.ts +2 -0
- package/src/command-context.test.ts +200 -0
- package/src/command-context.ts +137 -0
- package/src/core.ts +7 -0
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +256 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/grid-messages.ts +16 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +3 -2
- package/src/index.ts +150 -2
- package/src/keyboard-handlers.coverage.test.ts +126 -6
- package/src/keyboard-handlers.ts +91 -30
- package/src/keyboard-shortcuts.seam.test.ts +200 -0
- package/src/keyboard.test.ts +124 -1
- package/src/keyboard.ts +142 -0
- package/src/menus.test.ts +26 -0
- package/src/menus.ts +10 -3
- package/src/merges.test.ts +112 -0
- package/src/merges.ts +168 -0
- package/src/pivot.grid.test.ts +40 -5
- package/src/row-resize.test.ts +44 -0
- package/src/row-resize.ts +17 -0
- package/src/scroll-sync.test.ts +42 -1
- package/src/scroll-sync.ts +25 -0
- package/src/selection.multi-range.test.ts +34 -0
- package/src/selection.test.ts +30 -2
- package/src/selection.ts +111 -19
- package/src/shortcut-registry.test.ts +97 -0
- package/src/shortcut-registry.ts +181 -0
- package/src/svgrid-wrapper.types.ts +184 -13
- package/src/svgrid.behavior.test.ts +135 -0
- package/src/svgrid.charting.test.ts +513 -3
- package/src/svgrid.context-menu.test.ts +31 -0
- package/src/svgrid.new-features.wrapper.test.ts +10 -3
- package/src/virtualization/virtualizer.ts +39 -17
- package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
- package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
- package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
- package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
- package/dist/cdn/chart-CKJRCPOk.js +0 -1652
|
@@ -54,6 +54,10 @@ function makeCtx(overrides: Partial<any> = {}) {
|
|
|
54
54
|
extendSelection: vi.fn(),
|
|
55
55
|
setSelection: vi.fn(),
|
|
56
56
|
closeMenus: vi.fn(),
|
|
57
|
+
tabRunOrigin: null,
|
|
58
|
+
activeRangeRect: () => null,
|
|
59
|
+
isRowCollapsed: () => false,
|
|
60
|
+
collapsedColumns: {},
|
|
57
61
|
...overrides,
|
|
58
62
|
}
|
|
59
63
|
ctx._state = state
|
|
@@ -344,12 +348,32 @@ describe('onGridKeyDown / navigation', () => {
|
|
|
344
348
|
expect(ctx.extendSelection).not.toHaveBeenCalled()
|
|
345
349
|
})
|
|
346
350
|
|
|
347
|
-
it('Shift+ArrowRight extends the selection rectangle', () => {
|
|
348
|
-
const ctx = navCtx(
|
|
351
|
+
it('Shift+ArrowRight extends the selection rectangle and leaves the active cell', () => {
|
|
352
|
+
const ctx = navCtx({
|
|
353
|
+
selectionRange: { anchor: { rowIndex: 1, colIndex: 1 }, focus: { rowIndex: 1, colIndex: 1 } },
|
|
354
|
+
})
|
|
349
355
|
const { onGridKeyDown } = createKeyboard(ctx)
|
|
350
356
|
onGridKeyDown(rootKeyEvent({ key: 'ArrowRight', shiftKey: true }))
|
|
351
|
-
expect(ctx.setActiveCell).
|
|
357
|
+
expect(ctx.setActiveCell).not.toHaveBeenCalled()
|
|
352
358
|
expect(ctx.extendSelection).toHaveBeenCalledWith(1, 2)
|
|
359
|
+
expect(ctx.scrollActiveCellIntoView).toHaveBeenCalledWith(1, 2)
|
|
360
|
+
expect(ctx.setSelection).not.toHaveBeenCalled()
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('Shift+Home, Shift+PageDown and Ctrl+Shift+Home grow the range to where the plain key goes', () => {
|
|
364
|
+
const ctx = navCtx({
|
|
365
|
+
selectionRange: { anchor: { rowIndex: 1, colIndex: 1 }, focus: { rowIndex: 3, colIndex: 2 } },
|
|
366
|
+
})
|
|
367
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
368
|
+
onGridKeyDown(rootKeyEvent({ key: 'Home', shiftKey: true }))
|
|
369
|
+
expect(ctx.extendSelection).toHaveBeenLastCalledWith(3, 0)
|
|
370
|
+
onGridKeyDown(rootKeyEvent({ key: 'Home', shiftKey: true, ctrlKey: true }))
|
|
371
|
+
expect(ctx.extendSelection).toHaveBeenLastCalledWith(0, 0)
|
|
372
|
+
onGridKeyDown(rootKeyEvent({ key: 'PageDown', shiftKey: true }))
|
|
373
|
+
const [row, col] = ctx.extendSelection.mock.lastCall
|
|
374
|
+
expect(col).toBe(2)
|
|
375
|
+
expect(row).toBeGreaterThan(3)
|
|
376
|
+
expect(ctx.setActiveCell).not.toHaveBeenCalled()
|
|
353
377
|
expect(ctx.setSelection).not.toHaveBeenCalled()
|
|
354
378
|
})
|
|
355
379
|
|
|
@@ -364,13 +388,109 @@ describe('onGridKeyDown / navigation', () => {
|
|
|
364
388
|
expect(ctx.setSelection).toHaveBeenCalledWith(1, 0)
|
|
365
389
|
})
|
|
366
390
|
|
|
367
|
-
it('Shift+
|
|
391
|
+
it('Shift+Arrow grows from the far corner of the range, not from the active cell', () => {
|
|
392
|
+
const ctx = navCtx({
|
|
393
|
+
selectionRange: { anchor: { rowIndex: 1, colIndex: 1 }, focus: { rowIndex: 3, colIndex: 2 } },
|
|
394
|
+
})
|
|
395
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
396
|
+
onGridKeyDown(rootKeyEvent({ key: 'ArrowDown', shiftKey: true }))
|
|
397
|
+
expect(ctx.extendSelection).toHaveBeenCalledWith(4, 2)
|
|
398
|
+
expect(ctx.setActiveCell).not.toHaveBeenCalled()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('Shift+Arrow with nothing anchored anchors at the active cell first', () => {
|
|
402
|
+
const ctx = navCtx({ selectionRange: { anchor: null, focus: null } })
|
|
403
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
404
|
+
onGridKeyDown(rootKeyEvent({ key: 'ArrowDown', shiftKey: true }))
|
|
405
|
+
expect(ctx.setSelection).toHaveBeenCalledWith(1, 1)
|
|
406
|
+
expect(ctx.extendSelection).toHaveBeenCalledWith(2, 1)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('Shift+Arrow moves the active cell when cell selection is off', () => {
|
|
410
|
+
const ctx = navCtx({ enableCellSelectionEffective: false })
|
|
411
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
412
|
+
onGridKeyDown(rootKeyEvent({ key: 'ArrowDown', shiftKey: true }))
|
|
413
|
+
expect(ctx.setActiveCell).toHaveBeenCalledWith(2, 1)
|
|
414
|
+
expect(ctx.extendSelection).not.toHaveBeenCalled()
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('Ctrl+V with onPasteClipboard leaves the key to the browser and arms the async fallback', () => {
|
|
418
|
+
const arm = vi.fn()
|
|
419
|
+
const ctx = navCtx({ props: { rowHeight: 30, onPasteClipboard: () => true }, armPasteFallback: arm, pasteFromClipboard: vi.fn() })
|
|
420
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
421
|
+
const ev = rootKeyEvent({ key: 'v', ctrlKey: true })
|
|
422
|
+
onGridKeyDown(ev)
|
|
423
|
+
expect(ev.defaultPrevented).toBe(false)
|
|
424
|
+
expect(arm).toHaveBeenCalledTimes(1)
|
|
425
|
+
expect(ctx.pasteFromClipboard).not.toHaveBeenCalled()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('Shift+Enter moves up without extending the selection', () => {
|
|
429
|
+
// Shift+Enter shares the moveUp intent with Shift+ArrowUp, and used to
|
|
430
|
+
// grow the range like the arrow does. In Excel it only reverses Enter.
|
|
368
431
|
const ctx = navCtx()
|
|
369
432
|
const { onGridKeyDown } = createKeyboard(ctx)
|
|
370
433
|
onGridKeyDown(rootKeyEvent({ key: 'Enter', shiftKey: true }))
|
|
371
|
-
// Enter+shift -> moveUp, which is in the extend set.
|
|
372
434
|
expect(ctx.setActiveCell).toHaveBeenCalledWith(0, 1)
|
|
373
|
-
expect(ctx.
|
|
435
|
+
expect(ctx.setSelection).toHaveBeenCalledWith(0, 1)
|
|
436
|
+
expect(ctx.extendSelection).not.toHaveBeenCalled()
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('Enter after a run of Tabs goes down from the column the run began in', () => {
|
|
440
|
+
const ctx = navCtx({ state: { activeCell: { rowIndex: 0, colIndex: 0, cellId: null } } })
|
|
441
|
+
// The fake setActiveCell does what the real one does: forgets the run.
|
|
442
|
+
ctx.setActiveCell = vi.fn((r: number, c: number) => {
|
|
443
|
+
const prev = ctx._state.activeCell
|
|
444
|
+
if (!prev || prev.rowIndex !== r || prev.colIndex !== c) ctx.tabRunOrigin = null
|
|
445
|
+
ctx._state.activeCell = { rowIndex: r, colIndex: c, cellId: null }
|
|
446
|
+
})
|
|
447
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
448
|
+
onGridKeyDown(rootKeyEvent({ key: 'Tab' }))
|
|
449
|
+
onGridKeyDown(rootKeyEvent({ key: 'Tab' }))
|
|
450
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 0, colIndex: 2 })
|
|
451
|
+
expect(ctx.tabRunOrigin).toBe(0)
|
|
452
|
+
onGridKeyDown(rootKeyEvent({ key: 'Enter' }))
|
|
453
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 1, colIndex: 0 })
|
|
454
|
+
expect(ctx.tabRunOrigin).toBe(null)
|
|
455
|
+
expect(ctx.setSelection).toHaveBeenLastCalledWith(1, 0)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('an arrow between the Tabs ends the run', () => {
|
|
459
|
+
const ctx = navCtx({ state: { activeCell: { rowIndex: 0, colIndex: 0, cellId: null } } })
|
|
460
|
+
ctx.setActiveCell = vi.fn((r: number, c: number) => {
|
|
461
|
+
const prev = ctx._state.activeCell
|
|
462
|
+
if (!prev || prev.rowIndex !== r || prev.colIndex !== c) ctx.tabRunOrigin = null
|
|
463
|
+
ctx._state.activeCell = { rowIndex: r, colIndex: c, cellId: null }
|
|
464
|
+
})
|
|
465
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
466
|
+
onGridKeyDown(rootKeyEvent({ key: 'Tab' }))
|
|
467
|
+
onGridKeyDown(rootKeyEvent({ key: 'ArrowRight' }))
|
|
468
|
+
expect(ctx.tabRunOrigin).toBe(null)
|
|
469
|
+
onGridKeyDown(rootKeyEvent({ key: 'Enter' }))
|
|
470
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 1, colIndex: 2 })
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('Enter and Tab stay inside a selected block and keep the selection', () => {
|
|
474
|
+
const ctx = navCtx({
|
|
475
|
+
state: { activeCell: { rowIndex: 1, colIndex: 1, cellId: null } },
|
|
476
|
+
activeRangeRect: () => ({ minRow: 1, maxRow: 2, minCol: 1, maxCol: 2 }),
|
|
477
|
+
})
|
|
478
|
+
ctx.setActiveCell = vi.fn((r: number, c: number) => {
|
|
479
|
+
const prev = ctx._state.activeCell
|
|
480
|
+
if (!prev || prev.rowIndex !== r || prev.colIndex !== c) ctx.tabRunOrigin = null
|
|
481
|
+
ctx._state.activeCell = { rowIndex: r, colIndex: c, cellId: null }
|
|
482
|
+
})
|
|
483
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
484
|
+
onGridKeyDown(rootKeyEvent({ key: 'Enter' }))
|
|
485
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 2, colIndex: 1 })
|
|
486
|
+
onGridKeyDown(rootKeyEvent({ key: 'Enter' }))
|
|
487
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 1, colIndex: 2 })
|
|
488
|
+
onGridKeyDown(rootKeyEvent({ key: 'Tab' }))
|
|
489
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 2, colIndex: 1 })
|
|
490
|
+
onGridKeyDown(rootKeyEvent({ key: 'Tab', shiftKey: true }))
|
|
491
|
+
expect(ctx._state.activeCell).toMatchObject({ rowIndex: 1, colIndex: 2 })
|
|
492
|
+
expect(ctx.setSelection).not.toHaveBeenCalled()
|
|
493
|
+
expect(ctx.extendSelection).not.toHaveBeenCalled()
|
|
374
494
|
})
|
|
375
495
|
|
|
376
496
|
it('Tab moves right and wraps using getNextActiveCell', () => {
|
package/src/keyboard-handlers.ts
CHANGED
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
} from "./index";
|
|
8
8
|
import "./sv-grid-scrollbar";
|
|
9
9
|
|
|
10
|
-
import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
|
|
10
|
+
import { getKeyboardIntent, getNextActiveCell, getEntryStep, pastCollapsed } from "./keyboard";
|
|
11
|
+
import { stepPastMerge } from "./merges";
|
|
12
|
+
import { hasGridShortcuts, runGridShortcuts } from "./shortcut-registry";
|
|
13
|
+
import { undoHistory, redoHistory } from "./history";
|
|
14
|
+
import { buildCommandContext } from "./command-context";
|
|
11
15
|
|
|
12
16
|
export function createKeyboard<
|
|
13
17
|
TFeatures extends TableFeatures = TableFeatures,
|
|
@@ -16,7 +20,21 @@ export function createKeyboard<
|
|
|
16
20
|
function onGridKeyDown(event: KeyboardEvent) {
|
|
17
21
|
// Only the grid root drives navigation - keys on header buttons, menus,
|
|
18
22
|
// or the cell editor are handled by those controls themselves.
|
|
23
|
+
//
|
|
24
|
+
// The command chain runs AFTER this guard, not before. Running it first
|
|
25
|
+
// meant a key typed into a filter-row input reached the sheet commands,
|
|
26
|
+
// so Ctrl+A or Ctrl+D while filtering mutated grid cells. The editor has
|
|
27
|
+
// its own chain call in editing.ts, which is what Alt+Enter needs.
|
|
19
28
|
if (event.target !== event.currentTarget) return;
|
|
29
|
+
|
|
30
|
+
// Registered commands get the key BEFORE the grid interprets it. That is
|
|
31
|
+
// how @svgrid/enterprise binds Ctrl+Arrow, Ctrl+D and the rest without
|
|
32
|
+
// widening the closed GridKeyboardIntent union, which is public API. A
|
|
33
|
+
// grid with nothing registered pays one array-length read.
|
|
34
|
+
if (hasGridShortcuts() && runGridShortcuts(event, buildCommandContext(ctx, false))) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
if (ctx.editingCell) return;
|
|
21
39
|
|
|
22
40
|
if ((event.ctrlKey || event.metaKey) && !event.altKey) {
|
|
@@ -27,6 +45,14 @@ export function createKeyboard<
|
|
|
27
45
|
return;
|
|
28
46
|
}
|
|
29
47
|
if (lower === "v") {
|
|
48
|
+
// A consumer taking the paste over wants the browser's own `paste`
|
|
49
|
+
// event, which carries the clipboard's HTML unsanitised; the key is
|
|
50
|
+
// left alone and the async API is armed as the fallback for a
|
|
51
|
+
// browser that never delivers the event to a non-editable element.
|
|
52
|
+
if (ctx.props.onPasteClipboard) {
|
|
53
|
+
ctx.armPasteFallback();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
30
56
|
// Secure context: read via the async Clipboard API and swallow the
|
|
31
57
|
// key. Insecure context (no navigator.clipboard): DON'T preventDefault
|
|
32
58
|
// so the browser delivers a native `paste` event to `onGridPaste`.
|
|
@@ -45,26 +71,12 @@ export function createKeyboard<
|
|
|
45
71
|
// and Ctrl+Y both redo. Mirrors VSCode / Sheets / Excel.
|
|
46
72
|
if (lower === "z" && !event.shiftKey) {
|
|
47
73
|
event.preventDefault()
|
|
48
|
-
|
|
49
|
-
const step = ctx.history[ctx.historyPtr]
|
|
50
|
-
if (step) {
|
|
51
|
-
ctx.applyHistoryStep(step, 'undo')
|
|
52
|
-
ctx.historyPtr -= 1
|
|
53
|
-
ctx.historyVersion += 1
|
|
54
|
-
}
|
|
55
|
-
}
|
|
74
|
+
undoHistory(ctx)
|
|
56
75
|
return
|
|
57
76
|
}
|
|
58
77
|
if ((lower === "z" && event.shiftKey) || lower === "y") {
|
|
59
78
|
event.preventDefault()
|
|
60
|
-
|
|
61
|
-
const step = ctx.history[ctx.historyPtr + 1]
|
|
62
|
-
if (step) {
|
|
63
|
-
ctx.applyHistoryStep(step, 'redo')
|
|
64
|
-
ctx.historyPtr += 1
|
|
65
|
-
ctx.historyVersion += 1
|
|
66
|
-
}
|
|
67
|
-
}
|
|
79
|
+
redoHistory(ctx)
|
|
68
80
|
return
|
|
69
81
|
}
|
|
70
82
|
// Ctrl+F opens the find overlay.
|
|
@@ -204,25 +216,74 @@ export function createKeyboard<
|
|
|
204
216
|
const usable = Math.max(0, clientHeight - headerHeight)
|
|
205
217
|
return Math.max(1, Math.floor(usable / Math.max(rowHeight, 1)) - 1)
|
|
206
218
|
}
|
|
207
|
-
const
|
|
219
|
+
const bounds = {
|
|
208
220
|
maxRow: Math.max(ctx.allRows.length - 1, 0),
|
|
209
221
|
maxCol: Math.max(ctx.allColumns.length - 1, 0),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
222
|
+
};
|
|
223
|
+
// Collapsed rows and columns are stepped over, as a sheet's hidden ones.
|
|
224
|
+
const collapsed = {
|
|
225
|
+
isRowCollapsed: (i: number) => ctx.isRowCollapsed(i) as boolean,
|
|
226
|
+
isColumnCollapsed: (i: number) => !!ctx.collapsedColumns[ctx.allColumns[i]?.id],
|
|
227
|
+
};
|
|
228
|
+
// Enter and Tab are entry keys: they remember a Tab run and stay inside
|
|
229
|
+
// a selected block (getEntryStep). ArrowDown shares the moveDown intent
|
|
230
|
+
// and is a plain move, hence the key check.
|
|
231
|
+
if (
|
|
232
|
+
(event.key === "Enter" || event.key === "Tab") &&
|
|
233
|
+
(intent === "moveDown" || intent === "moveUp" || intent === "tabNext" || intent === "tabPrev")
|
|
234
|
+
) {
|
|
235
|
+
const step = getEntryStep(current, intent, {
|
|
236
|
+
...bounds,
|
|
237
|
+
tabOrigin: ctx.tabRunOrigin,
|
|
238
|
+
range: ctx.activeRangeRect(),
|
|
239
|
+
collapsed,
|
|
240
|
+
});
|
|
241
|
+
const landed = stepPastMerge(ctx.mergeIndex, current, step.cell, bounds);
|
|
242
|
+
ctx.setActiveCell(landed.rowIndex, landed.colIndex);
|
|
243
|
+
ctx.tabRunOrigin = step.tabOrigin;
|
|
244
|
+
ctx.scrollActiveCellIntoView(landed.rowIndex, landed.colIndex);
|
|
245
|
+
if (!step.withinRange) ctx.setSelection(landed.rowIndex, landed.colIndex);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Shift+Arrow grows the range from its far corner, and the active cell
|
|
249
|
+
// stays where it is, as in Excel: it is the cell the formula bar shows
|
|
250
|
+
// and the one the next keystroke edits. Shift+Home, Shift+End,
|
|
251
|
+
// Shift+PageUp / PageDown and Ctrl+Shift+Home / End grow it the same
|
|
252
|
+
// way, to where the plain key would have moved. Shift+Enter and
|
|
253
|
+
// Shift+Tab never come this way (they are entry keys, above), so
|
|
254
|
+
// hammering Shift+Tab back through a row cannot paint a creeping
|
|
255
|
+
// rectangle. A grid without cell selection has no range to grow and
|
|
256
|
+
// moves the active cell instead.
|
|
257
|
+
const extending =
|
|
219
258
|
event.shiftKey &&
|
|
259
|
+
ctx.enableCellSelectionEffective !== false &&
|
|
220
260
|
(intent === "moveLeft" ||
|
|
221
261
|
intent === "moveRight" ||
|
|
222
262
|
intent === "moveUp" ||
|
|
223
|
-
intent === "moveDown"
|
|
224
|
-
|
|
225
|
-
|
|
263
|
+
intent === "moveDown" ||
|
|
264
|
+
intent === "rowStart" ||
|
|
265
|
+
intent === "rowEnd" ||
|
|
266
|
+
intent === "pageUp" ||
|
|
267
|
+
intent === "pageDown" ||
|
|
268
|
+
intent === "gridStart" ||
|
|
269
|
+
intent === "gridEnd");
|
|
270
|
+
if (extending) {
|
|
271
|
+
const anchored = !!ctx.selectionRange?.anchor;
|
|
272
|
+
if (!anchored) ctx.setSelection(current.rowIndex, current.colIndex);
|
|
273
|
+
const from = anchored ? ctx.selectionRange.focus ?? current : current;
|
|
274
|
+
const next = stepPastMerge(ctx.mergeIndex, from, pastCollapsed(from, getNextActiveCell(from, intent, { ...bounds, pageSize: pageStep() }), bounds, collapsed), bounds);
|
|
275
|
+
ctx.extendSelection(next.rowIndex, next.colIndex);
|
|
276
|
+
// Scroll along the axis of the key only, as Excel does: Shift+Right
|
|
277
|
+
// brings the new column into view without moving the rows, so a
|
|
278
|
+
// whole-column selection (its far corner on the last row) stays put.
|
|
279
|
+
const sideways = intent === "moveLeft" || intent === "moveRight" || intent === "rowStart" || intent === "rowEnd";
|
|
280
|
+
ctx.scrollActiveCellIntoView(sideways ? current.rowIndex : next.rowIndex, sideways ? next.colIndex : current.colIndex);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const next = stepPastMerge(ctx.mergeIndex, current, pastCollapsed(current, getNextActiveCell(current, intent, { ...bounds, pageSize: pageStep() }), bounds, collapsed), bounds);
|
|
284
|
+
ctx.setActiveCell(next.rowIndex, next.colIndex);
|
|
285
|
+
ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
|
|
286
|
+
ctx.setSelection(next.rowIndex, next.colIndex);
|
|
226
287
|
}
|
|
227
288
|
|
|
228
289
|
function onWindowKeydown(event: KeyboardEvent) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { createKeyboard } from './keyboard-handlers'
|
|
3
|
+
import { getKeyboardIntent } from './keyboard'
|
|
4
|
+
import { registerGridShortcuts, clearGridShortcuts } from './shortcut-registry'
|
|
5
|
+
|
|
6
|
+
// Same harness shape as keyboard-handlers.coverage.test.ts: a fake controller
|
|
7
|
+
// ctx that records side effects, and real jsdom KeyboardEvents so modifier
|
|
8
|
+
// flags and target/currentTarget behave.
|
|
9
|
+
function makeCtx(overrides: Partial<any> = {}) {
|
|
10
|
+
const state: any = { activeCell: { rowIndex: 2, colIndex: 1, cellId: null }, sorting: [] }
|
|
11
|
+
const ctx: any = {
|
|
12
|
+
editingCell: null,
|
|
13
|
+
findOpen: false,
|
|
14
|
+
history: [],
|
|
15
|
+
historyPtr: -1,
|
|
16
|
+
historyVersion: 0,
|
|
17
|
+
historyGroupId: undefined,
|
|
18
|
+
UNDO_LIMIT: 200,
|
|
19
|
+
headerHeight: 30,
|
|
20
|
+
scrollContainer: { clientHeight: 330 },
|
|
21
|
+
props: { rowHeight: 30 },
|
|
22
|
+
allRows: [{}, {}, {}, {}],
|
|
23
|
+
allColumns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
24
|
+
grid: { getState: () => state, store: { setState: vi.fn() } },
|
|
25
|
+
buildApi: () => ({}),
|
|
26
|
+
getSelectionRects: () => [{ minRow: 2, minCol: 1, maxRow: 2, maxCol: 1 }],
|
|
27
|
+
readCellRaw: vi.fn(() => 'v'),
|
|
28
|
+
writeCellRaw: vi.fn(),
|
|
29
|
+
copySelectionToClipboard: vi.fn(),
|
|
30
|
+
pasteFromClipboard: vi.fn(() => Promise.resolve()),
|
|
31
|
+
cutSelectionToClipboard: vi.fn(() => Promise.resolve()),
|
|
32
|
+
applyHistoryStep: vi.fn(),
|
|
33
|
+
clearSelectedCells: vi.fn(() => false),
|
|
34
|
+
clearSelectedCellValues: vi.fn(),
|
|
35
|
+
onCellDoubleClick: vi.fn(),
|
|
36
|
+
startEditingWithChar: vi.fn(() => true),
|
|
37
|
+
toggleBooleanCell: vi.fn(),
|
|
38
|
+
setActiveCell: vi.fn(),
|
|
39
|
+
scrollActiveCellIntoView: vi.fn(),
|
|
40
|
+
extendSelection: vi.fn(),
|
|
41
|
+
setSelection: vi.fn(),
|
|
42
|
+
closeMenus: vi.fn(),
|
|
43
|
+
tabRunOrigin: null,
|
|
44
|
+
activeRangeRect: () => null,
|
|
45
|
+
isRowCollapsed: () => false,
|
|
46
|
+
collapsedColumns: {},
|
|
47
|
+
...overrides,
|
|
48
|
+
}
|
|
49
|
+
return ctx
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function rootKeyEvent(init: KeyboardEventInit) {
|
|
53
|
+
const el = document.createElement('div')
|
|
54
|
+
const event = new KeyboardEvent('keydown', { cancelable: true, ...init })
|
|
55
|
+
Object.defineProperty(event, 'target', { value: el })
|
|
56
|
+
Object.defineProperty(event, 'currentTarget', { value: el })
|
|
57
|
+
return event
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeEach(() => clearGridShortcuts())
|
|
61
|
+
|
|
62
|
+
describe('the shortcut chain inside onGridKeyDown', () => {
|
|
63
|
+
it('lets a handler claim a key the grid would otherwise interpret', () => {
|
|
64
|
+
registerGridShortcuts((e) => e.key === 'ArrowDown' && e.ctrlKey)
|
|
65
|
+
const ctx = makeCtx()
|
|
66
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
67
|
+
onGridKeyDown(rootKeyEvent({ key: 'ArrowDown', ctrlKey: true }))
|
|
68
|
+
expect(ctx.setActiveCell).not.toHaveBeenCalled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('claims a key the grid DOES implement, so enterprise can reclaim it', () => {
|
|
72
|
+
// Ctrl+C is copy in the free grid. A registered handler taking it first is
|
|
73
|
+
// what lets a sheet put formulas and formats on the clipboard instead.
|
|
74
|
+
registerGridShortcuts(() => true)
|
|
75
|
+
const ctx = makeCtx()
|
|
76
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
77
|
+
onGridKeyDown(rootKeyEvent({ key: 'c', ctrlKey: true }))
|
|
78
|
+
expect(ctx.copySelectionToClipboard).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls through to normal navigation when the handler declines', () => {
|
|
82
|
+
registerGridShortcuts(() => false)
|
|
83
|
+
const ctx = makeCtx()
|
|
84
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
85
|
+
onGridKeyDown(rootKeyEvent({ key: 'ArrowDown' }))
|
|
86
|
+
expect(ctx.setActiveCell).toHaveBeenCalledWith(3, 1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does NOT run for a key typed into a child element', () => {
|
|
90
|
+
// The chain used to run before the grid-root guard, so a Ctrl+A or Ctrl+D
|
|
91
|
+
// typed into a filter-row input reached the sheet commands and mutated
|
|
92
|
+
// grid cells.
|
|
93
|
+
const handler = vi.fn(() => true)
|
|
94
|
+
registerGridShortcuts(handler)
|
|
95
|
+
const ctx = makeCtx()
|
|
96
|
+
const root = document.createElement('div')
|
|
97
|
+
const input = document.createElement('input')
|
|
98
|
+
const event = new KeyboardEvent('keydown', { key: 'd', ctrlKey: true, cancelable: true })
|
|
99
|
+
Object.defineProperty(event, 'target', { value: input })
|
|
100
|
+
Object.defineProperty(event, 'currentTarget', { value: root })
|
|
101
|
+
createKeyboard(ctx).onGridKeyDown(event)
|
|
102
|
+
expect(handler).not.toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('runs before the editing bail, so a mid-edit key still reaches a handler', () => {
|
|
106
|
+
const handler = vi.fn(() => true)
|
|
107
|
+
registerGridShortcuts(handler)
|
|
108
|
+
const ctx = makeCtx({ editingCell: { rowId: 'r1', columnId: 'a' } })
|
|
109
|
+
const { onGridKeyDown } = createKeyboard(ctx)
|
|
110
|
+
onGridKeyDown(rootKeyEvent({ key: 'Enter', altKey: true }))
|
|
111
|
+
expect(handler).toHaveBeenCalledOnce()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('hands the handler a context describing the active cell and selection', () => {
|
|
115
|
+
let seen: any = null
|
|
116
|
+
registerGridShortcuts((_e, cmd) => {
|
|
117
|
+
seen = {
|
|
118
|
+
active: cmd.activeCell,
|
|
119
|
+
ranges: cmd.ranges,
|
|
120
|
+
rowCount: cmd.rowCount,
|
|
121
|
+
colCount: cmd.colCount,
|
|
122
|
+
editing: cmd.editing,
|
|
123
|
+
}
|
|
124
|
+
return true
|
|
125
|
+
})
|
|
126
|
+
const ctx = makeCtx()
|
|
127
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'd', ctrlKey: true }))
|
|
128
|
+
expect(seen.active).toEqual({ rowIndex: 2, colIndex: 1, columnId: 'b' })
|
|
129
|
+
expect(seen.ranges).toEqual([[2, 1, 2, 1]])
|
|
130
|
+
expect(seen.rowCount).toBe(4)
|
|
131
|
+
expect(seen.colCount).toBe(3)
|
|
132
|
+
expect(seen.editing).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('reads and writes cells by display index through the context', () => {
|
|
136
|
+
registerGridShortcuts((_e, cmd) => {
|
|
137
|
+
cmd.setCellValue(0, 2, cmd.getCellValue(1, 0))
|
|
138
|
+
return true
|
|
139
|
+
})
|
|
140
|
+
const ctx = makeCtx()
|
|
141
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'd', ctrlKey: true }))
|
|
142
|
+
expect(ctx.readCellRaw).toHaveBeenCalledWith(1, 'a')
|
|
143
|
+
expect(ctx.writeCellRaw).toHaveBeenCalledWith(0, 'c', 'v')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('does not build a context when nothing is registered', () => {
|
|
147
|
+
const ctx = makeCtx()
|
|
148
|
+
// getSelectionRects is only reached through the command context.
|
|
149
|
+
ctx.getSelectionRects = vi.fn(() => [])
|
|
150
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'ArrowDown' }))
|
|
151
|
+
expect(ctx.getSelectionRects).not.toHaveBeenCalled()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('Ctrl / Cmd + arrow', () => {
|
|
156
|
+
it('is a noop intent rather than a single-cell move', () => {
|
|
157
|
+
for (const key of ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']) {
|
|
158
|
+
expect(getKeyboardIntent(new KeyboardEvent('keydown', { key, ctrlKey: true }))).toBe('noop')
|
|
159
|
+
expect(getKeyboardIntent(new KeyboardEvent('keydown', { key, metaKey: true }))).toBe('noop')
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('leaves a plain arrow alone', () => {
|
|
164
|
+
expect(getKeyboardIntent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))).toBe('moveDown')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('leaves Alt combinations to the browser', () => {
|
|
168
|
+
expect(
|
|
169
|
+
getKeyboardIntent(new KeyboardEvent('keydown', { key: 'ArrowLeft', ctrlKey: true, altKey: true })),
|
|
170
|
+
).toBe('moveLeft')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('does not move the active cell when unclaimed', () => {
|
|
174
|
+
const ctx = makeCtx()
|
|
175
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'ArrowDown', ctrlKey: true }))
|
|
176
|
+
expect(ctx.setActiveCell).not.toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('still does not start an edit, since the key is not printable', () => {
|
|
180
|
+
const ctx = makeCtx()
|
|
181
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'ArrowDown', ctrlKey: true }))
|
|
182
|
+
expect(ctx.startEditingWithChar).not.toHaveBeenCalled()
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('grouped undo through the keyboard', () => {
|
|
187
|
+
it('walks a whole grouped action back on one Ctrl+Z', () => {
|
|
188
|
+
const g = 'g1'
|
|
189
|
+
const ctx = makeCtx({
|
|
190
|
+
history: [
|
|
191
|
+
{ rowId: 'r1', columnId: 'a', field: 'a', before: 1, after: 2, groupId: g },
|
|
192
|
+
{ rowId: 'r2', columnId: 'a', field: 'a', before: 3, after: 4, groupId: g },
|
|
193
|
+
],
|
|
194
|
+
historyPtr: 1,
|
|
195
|
+
})
|
|
196
|
+
createKeyboard(ctx).onGridKeyDown(rootKeyEvent({ key: 'z', ctrlKey: true }))
|
|
197
|
+
expect(ctx.applyHistoryStep).toHaveBeenCalledTimes(2)
|
|
198
|
+
expect(ctx.historyPtr).toBe(-1)
|
|
199
|
+
})
|
|
200
|
+
})
|
package/src/keyboard.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { getKeyboardIntent, getNextActiveCell } from './keyboard'
|
|
2
|
+
import { getKeyboardIntent, getNextActiveCell, getEntryStep, pastCollapsed } from './keyboard'
|
|
3
|
+
import type { ActiveCellState } from './core'
|
|
3
4
|
|
|
4
5
|
describe('keyboard navigation engine', () => {
|
|
5
6
|
it('maps keys to navigation intents', () => {
|
|
@@ -57,3 +58,125 @@ describe('keyboard navigation engine', () => {
|
|
|
57
58
|
expect(pageDown.rowIndex).toBe(20)
|
|
58
59
|
})
|
|
59
60
|
})
|
|
61
|
+
|
|
62
|
+
describe('getEntryStep', () => {
|
|
63
|
+
const at = (rowIndex: number, colIndex: number): ActiveCellState => ({ rowIndex, colIndex, cellId: null })
|
|
64
|
+
const open = { maxRow: 9, maxCol: 9, range: null }
|
|
65
|
+
|
|
66
|
+
it('a Tab starts a run at the column it leaves, and later Tabs keep it', () => {
|
|
67
|
+
const first = getEntryStep(at(0, 0), 'tabNext', { ...open, tabOrigin: null })
|
|
68
|
+
expect(first.cell).toMatchObject({ rowIndex: 0, colIndex: 1 })
|
|
69
|
+
expect(first.tabOrigin).toBe(0)
|
|
70
|
+
const second = getEntryStep(first.cell, 'tabNext', { ...open, tabOrigin: first.tabOrigin })
|
|
71
|
+
expect(second.cell).toMatchObject({ rowIndex: 0, colIndex: 2 })
|
|
72
|
+
expect(second.tabOrigin).toBe(0)
|
|
73
|
+
const back = getEntryStep(second.cell, 'tabPrev', { ...open, tabOrigin: second.tabOrigin })
|
|
74
|
+
expect(back.cell).toMatchObject({ rowIndex: 0, colIndex: 1 })
|
|
75
|
+
expect(back.tabOrigin).toBe(0)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('Enter after a run goes down from the run\'s first column and ends it', () => {
|
|
79
|
+
const step = getEntryStep(at(0, 2), 'moveDown', { ...open, tabOrigin: 0 })
|
|
80
|
+
expect(step.cell).toMatchObject({ rowIndex: 1, colIndex: 0 })
|
|
81
|
+
expect(step.tabOrigin).toBe(null)
|
|
82
|
+
expect(step.withinRange).toBe(false)
|
|
83
|
+
const up = getEntryStep(at(3, 5), 'moveUp', { ...open, tabOrigin: 2 })
|
|
84
|
+
expect(up.cell).toMatchObject({ rowIndex: 2, colIndex: 2 })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('Enter with no run is a plain move down', () => {
|
|
88
|
+
const step = getEntryStep(at(0, 2), 'moveDown', { ...open, tabOrigin: null })
|
|
89
|
+
expect(step.cell).toMatchObject({ rowIndex: 1, colIndex: 2 })
|
|
90
|
+
expect(step.tabOrigin).toBe(null)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('walks a selected block: Enter down the columns, Tab along the rows, both wrapping', () => {
|
|
94
|
+
const range = { minRow: 1, maxRow: 2, minCol: 1, maxCol: 2 }
|
|
95
|
+
const opts = { maxRow: 9, maxCol: 9, tabOrigin: null, range }
|
|
96
|
+
const path = (start: ActiveCellState, intent: 'moveDown' | 'moveUp' | 'tabNext' | 'tabPrev', n: number) => {
|
|
97
|
+
const out: Array<[number, number]> = []
|
|
98
|
+
let cell = start
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
const step = getEntryStep(cell, intent, opts)
|
|
101
|
+
expect(step.withinRange).toBe(true)
|
|
102
|
+
expect(step.tabOrigin).toBe(null)
|
|
103
|
+
cell = step.cell
|
|
104
|
+
out.push([cell.rowIndex, cell.colIndex])
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
expect(path(at(1, 1), 'moveDown', 4)).toEqual([[2, 1], [1, 2], [2, 2], [1, 1]])
|
|
109
|
+
expect(path(at(1, 1), 'moveUp', 4)).toEqual([[2, 2], [1, 2], [2, 1], [1, 1]])
|
|
110
|
+
expect(path(at(1, 1), 'tabNext', 4)).toEqual([[1, 2], [2, 1], [2, 2], [1, 1]])
|
|
111
|
+
expect(path(at(1, 1), 'tabPrev', 4)).toEqual([[2, 2], [2, 1], [1, 2], [1, 1]])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('a one-cell range and a cursor outside the range are not a block', () => {
|
|
115
|
+
const single = getEntryStep(at(1, 1), 'moveDown', { ...open, tabOrigin: null, range: { minRow: 1, maxRow: 1, minCol: 1, maxCol: 1 } })
|
|
116
|
+
expect(single.withinRange).toBe(false)
|
|
117
|
+
expect(single.cell).toMatchObject({ rowIndex: 2, colIndex: 1 })
|
|
118
|
+
const outside = getEntryStep(at(5, 5), 'tabNext', { ...open, tabOrigin: null, range: { minRow: 1, maxRow: 2, minCol: 1, maxCol: 2 } })
|
|
119
|
+
expect(outside.withinRange).toBe(false)
|
|
120
|
+
expect(outside.cell).toMatchObject({ rowIndex: 5, colIndex: 6 })
|
|
121
|
+
expect(outside.tabOrigin).toBe(5)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('a Tab run inside a block does not start a run', () => {
|
|
125
|
+
const range = { minRow: 0, maxRow: 1, minCol: 0, maxCol: 1 }
|
|
126
|
+
const step = getEntryStep(at(0, 0), 'tabNext', { ...open, tabOrigin: null, range })
|
|
127
|
+
expect(step.tabOrigin).toBe(null)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('steps over collapsed cells, on the open sheet and inside a block', () => {
|
|
131
|
+
const collapsed = { isRowCollapsed: (r: number) => r === 1, isColumnCollapsed: (c: number) => c === 1 }
|
|
132
|
+
const down = getEntryStep(at(0, 0), 'moveDown', { ...open, tabOrigin: null, collapsed })
|
|
133
|
+
expect(down.cell).toMatchObject({ rowIndex: 2, colIndex: 0 })
|
|
134
|
+
const tab = getEntryStep(at(0, 0), 'tabNext', { ...open, tabOrigin: null, collapsed })
|
|
135
|
+
expect(tab.cell).toMatchObject({ rowIndex: 0, colIndex: 2 })
|
|
136
|
+
expect(tab.tabOrigin).toBe(0)
|
|
137
|
+
// In a 3x3 block with its middle row and column hidden, Enter from the
|
|
138
|
+
// top-left goes to the bottom-left, then wraps to the top of column 3.
|
|
139
|
+
const range = { minRow: 0, maxRow: 2, minCol: 0, maxCol: 2 }
|
|
140
|
+
const first = getEntryStep(at(0, 0), 'moveDown', { ...open, tabOrigin: null, range, collapsed })
|
|
141
|
+
expect(first.cell).toMatchObject({ rowIndex: 2, colIndex: 0 })
|
|
142
|
+
expect(first.withinRange).toBe(true)
|
|
143
|
+
const second = getEntryStep(first.cell, 'moveDown', { ...open, tabOrigin: null, range, collapsed })
|
|
144
|
+
expect(second.cell).toMatchObject({ rowIndex: 0, colIndex: 2 })
|
|
145
|
+
// A block with nothing showing leaves the cursor where it is.
|
|
146
|
+
const dark = { isRowCollapsed: () => true, isColumnCollapsed: () => false }
|
|
147
|
+
const stuck = getEntryStep(at(0, 0), 'moveDown', { ...open, tabOrigin: null, range, collapsed: dark })
|
|
148
|
+
expect(stuck.cell).toMatchObject({ rowIndex: 0, colIndex: 0 })
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('pastCollapsed', () => {
|
|
153
|
+
const at = (rowIndex: number, colIndex: number): ActiveCellState => ({ rowIndex, colIndex, cellId: null })
|
|
154
|
+
const bounds = { maxRow: 5, maxCol: 5 }
|
|
155
|
+
const collapsed = (rows: number[], cols: number[]) => ({
|
|
156
|
+
isRowCollapsed: (r: number) => rows.includes(r),
|
|
157
|
+
isColumnCollapsed: (c: number) => cols.includes(c),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('goes on in the direction of travel to the next visible line', () => {
|
|
161
|
+
expect(pastCollapsed(at(0, 0), at(0, 1), bounds, collapsed([], [1, 2]))).toMatchObject({ rowIndex: 0, colIndex: 3 })
|
|
162
|
+
expect(pastCollapsed(at(3, 0), at(2, 0), bounds, collapsed([2, 1], []))).toMatchObject({ rowIndex: 0, colIndex: 0 })
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('comes back to the nearest visible line when the edge is hidden: End on a sheet whose last columns are hidden', () => {
|
|
166
|
+
expect(pastCollapsed(at(0, 0), at(0, 5), bounds, collapsed([], [4, 5]))).toMatchObject({ rowIndex: 0, colIndex: 3 })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('stays put when everything in the direction of travel is hidden', () => {
|
|
170
|
+
expect(pastCollapsed(at(0, 3), at(0, 4), bounds, collapsed([], [4, 5]))).toMatchObject({ rowIndex: 0, colIndex: 3 })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('leaves an axis the move did not touch alone', () => {
|
|
174
|
+
// The cursor sits on a row the user just hid; a sideways move keeps the row.
|
|
175
|
+
expect(pastCollapsed(at(2, 0), at(2, 1), bounds, collapsed([2], []))).toMatchObject({ rowIndex: 2, colIndex: 1 })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('returns the landing itself when nothing is hidden', () => {
|
|
179
|
+
const next = at(1, 1)
|
|
180
|
+
expect(pastCollapsed(at(0, 0), next, bounds, collapsed([], []))).toBe(next)
|
|
181
|
+
})
|
|
182
|
+
})
|