Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(sheets): prevent double-character on first cell keypress (#693)

Missing preventDefault in src/sheets/keyboard-handler.ts printable-char branch caused every first keypress against a cell to be doubled. Fix + 3 regression tests.

Closes #693

scott 20cfb10e ba163d59

+121 -1
+3
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ### Fixed 11 + - Sheets: first printable keystroke against an empty cell no longer duplicates the character (v0.62.1, #693). The grid's keydown handler for printable chars in `src/sheets/keyboard-handler.ts` entered edit mode and set `editor.value = key` but never called `e.preventDefault()`, so the browser's native keypress/input pipeline on the now-focused cell-editor input also inserted the same character — pressing `5` produced `55`, pressing `1`→`0`→Enter produced `110`. Added the missing `preventDefault()` plus a targeted regression test in `tests/sheets-keyboard-handler.test.ts` (3 new tests: preventDefault invariant, single-char-not-doubled invariant, and the pre-existing Cmd+key-skip invariant). Caught live by driving the deployed app in a real browser via Playwright MCP during TipTap v3 post-ship smoke testing. (#693) 12 + 10 13 ### Changed 11 14 - TipTap v2 → v3 coordinated upgrade (v0.62.0, #692) — bumped all 24 `@tiptap/*` packages from v2.11/v2.27 to v3.22.3 in a single coordinated release, per TipTap's guidance that mixing major versions leads to undefined behavior. Code changes required by v3: 12 15 - `@tiptap/extension-collaboration-cursor` was renamed to `@tiptap/extension-collaboration-caret` (the package was retired in v3); import and usage in `src/docs/main.ts` updated to the new `CollaborationCaret` extension.
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.62.0", 3 + "version": "0.62.1", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js",
+4
src/sheets/keyboard-handler.ts
··· 138 138 else if (key === 'Delete' || key === 'Backspace') { e.preventDefault(); deps.deleteSelectedCells(); } 139 139 else if (key === 'F2') { e.preventDefault(); deps.startEditing(deps.getSelectedCell().col, deps.getSelectedCell().row); } 140 140 else if (key.length === 1 && !e.ctrlKey && !e.metaKey) { 141 + // preventDefault is critical: without it, the browser's native keypress/input 142 + // handling on the now-focused cell-editor input inserts the same character 143 + // again after we set `editor.value = key`, producing a doubled character. 144 + e.preventDefault(); 141 145 const selectedCell = deps.getSelectedCell(); 142 146 deps.startEditing(selectedCell.col, selectedCell.row); 143 147 const editor = deps.grid.querySelector('.cell-editor') as HTMLInputElement | null;
+113
tests/sheets-keyboard-handler.test.ts
··· 1 + /** 2 + * @vitest-environment jsdom 3 + */ 4 + import { describe, it, expect, beforeEach, vi } from 'vitest'; 5 + import { wireKeyboardHandler, type KeyboardHandlerDeps } from '../src/sheets/keyboard-handler.js'; 6 + 7 + function makeDeps(overrides: Partial<KeyboardHandlerDeps> = {}): KeyboardHandlerDeps { 8 + const grid = document.createElement('div'); 9 + grid.className = 'sheet-grid'; 10 + document.body.appendChild(grid); 11 + const formulaInput = document.createElement('input'); 12 + formulaInput.className = 'formula-input'; 13 + document.body.appendChild(formulaInput); 14 + return { 15 + grid, 16 + formulaInput, 17 + sheetContainer: null, 18 + provider: { _saveSnapshot: () => {} }, 19 + getSelectedCell: () => ({ col: 1, row: 1 }), 20 + setSelectedCell: () => {}, 21 + getSelectionRange: () => null, 22 + setSelectionRange: () => {}, 23 + getActiveSheet: () => ({ get: () => 100 }), 24 + getCellData: () => null, 25 + setCellData: () => {}, 26 + getEditingCell: () => null, 27 + startEditing: () => {}, 28 + moveSelection: () => {}, 29 + extendSelection: () => {}, 30 + moveSelectionTo: () => {}, 31 + getDataExtent: () => ({ col: 1, row: 1 }), 32 + updateSelectionVisuals: () => {}, 33 + updateStatusBar: () => {}, 34 + copySelection: () => {}, 35 + deleteSelectedCells: () => {}, 36 + showPasteSpecialDialog: () => {}, 37 + applyStyleToSelection: () => {}, 38 + clearFormattingSelection: () => {}, 39 + updateUnderlineButtonState: () => {}, 40 + updateStrikethroughButtonState: () => {}, 41 + undoManager: null, 42 + evalCache: { clear: () => {} }, 43 + clearSpillMaps: () => {}, 44 + invalidateRecalcEngine: () => {}, 45 + refreshVisibleCells: () => {}, 46 + renderGrid: () => {}, 47 + hideSelectedRows: () => {}, 48 + hideSelectedCols: () => {}, 49 + unhideAdjacentRows: () => {}, 50 + unhideAdjacentCols: () => {}, 51 + toggleFilterMode: () => {}, 52 + printSheet: () => {}, 53 + showFindReplaceBar: () => {}, 54 + DEFAULT_COLS: 26, 55 + DEFAULT_ROWS: 100, 56 + ...overrides, 57 + }; 58 + } 59 + 60 + describe('wireKeyboardHandler — printable char starts cell edit', () => { 61 + beforeEach(() => { 62 + document.body.innerHTML = ''; 63 + }); 64 + 65 + it('calls preventDefault on a printable keydown so the browser does not also insert the char (regression: #693)', () => { 66 + const startEditing = vi.fn((_col: number, _row: number) => { 67 + // startEditing attaches the cell-editor input to the grid; simulate that 68 + const grid = document.querySelector('.sheet-grid')!; 69 + const input = document.createElement('input'); 70 + input.className = 'cell-editor'; 71 + grid.appendChild(input); 72 + }); 73 + 74 + const deps = makeDeps({ startEditing }); 75 + wireKeyboardHandler(deps); 76 + 77 + const ev = new KeyboardEvent('keydown', { key: '5', bubbles: true, cancelable: true }); 78 + const pd = vi.spyOn(ev, 'preventDefault'); 79 + document.dispatchEvent(ev); 80 + 81 + expect(startEditing).toHaveBeenCalledWith(1, 1); 82 + expect(pd).toHaveBeenCalled(); 83 + const editor = document.querySelector('.cell-editor') as HTMLInputElement; 84 + expect(editor.value).toBe('5'); 85 + }); 86 + 87 + it('sets editor.value to the typed key (not duplicated)', () => { 88 + const deps = makeDeps({ 89 + startEditing: () => { 90 + const input = document.createElement('input'); 91 + input.className = 'cell-editor'; 92 + document.querySelector('.sheet-grid')!.appendChild(input); 93 + }, 94 + }); 95 + wireKeyboardHandler(deps); 96 + 97 + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', bubbles: true, cancelable: true })); 98 + 99 + const editor = document.querySelector('.cell-editor') as HTMLInputElement; 100 + expect(editor.value).toBe('A'); 101 + expect(editor.value.length).toBe(1); 102 + }); 103 + 104 + it('does not start editing on Cmd+key (reserved for shortcuts)', () => { 105 + const startEditing = vi.fn(); 106 + const deps = makeDeps({ startEditing }); 107 + wireKeyboardHandler(deps); 108 + 109 + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', metaKey: true, bubbles: true, cancelable: true })); 110 + 111 + expect(startEditing).not.toHaveBeenCalled(); 112 + }); 113 + });