diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e909d60..db2cd584e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,13 @@ The release run heads these entries with the version and opens a fresh it, as a second set of glyphs over the drawn ones. That layer now stays transparent under `::selection`. +- A tap opens a sheet cell's editor where the pointer is coarse, so a phone + needs one tap rather than a double tap. A mouse keeps the double click. + +- **Fix**: a second click on a pinned cell cleared the pin while the sheet was + being edited, so a reader who tapped a cell twice lost it. A cell now holds + its pin there; a row or column header still clears on that click. + ## v7.1.0 - 2026-09-19 - The pdf view reports the pending annotation count on diff --git a/src/odr/internal/html/frontend/sheet-editing.js b/src/odr/internal/html/frontend/sheet-editing.js index ad3c58eac..89e69731a 100644 --- a/src/odr/internal/html/frontend/sheet-editing.js +++ b/src/odr/internal/html/frontend/sheet-editing.js @@ -405,12 +405,29 @@ } }); + /// A coarse pointer opens the editor on one tap: a phone gives up the first + /// tap of a double tap as a click of its own anyway. A fine pointer keeps the + /// double click, because there a single click selects a cell without editing. + function tapEdits() { + return ( + typeof window.matchMedia === "function" && + window.matchMedia("(pointer: coarse)").matches + ); + } + // A locked cell says so on the click, not on the double click. table.addEventListener("click", function (event) { var at = odr.editing.isEnabled() && overlay === null ? targetPosition(event) : null; - if (at !== null && odr.editing.lockAt(at.column, at.row) !== null) { + if (at === null) { + return; + } + if (odr.editing.lockAt(at.column, at.row) !== null) { odr.editing.refuseAt(at.column, at.row); + return; + } + if (tapEdits()) { + edit(at.column, at.row, null); } }); diff --git a/src/odr/internal/html/frontend/spreadsheet.js b/src/odr/internal/html/frontend/spreadsheet.js index 5b736904d..2d6a0be8e 100644 --- a/src/odr/internal/html/frontend/spreadsheet.js +++ b/src/odr/internal/html/frontend/spreadsheet.js @@ -455,6 +455,16 @@ paint(); }); + /// Whether the sheet is in the mode that writes cells. The editor is a + /// script of its own, and a read-only view carries none of it. + function editingEnabled() { + return ( + odr.editing !== undefined && + typeof odr.editing.isEnabled === "function" && + odr.editing.isEnabled() + ); + } + table.addEventListener("click", function (event) { // Selecting inside what is raised must not put the cell back. if (raisedContent !== null && raisedContent.contains(event.target)) { @@ -468,9 +478,12 @@ // Clicking what is pinned clears it - but `detail` counts the clicks, and // the second of a double click is the reader selecting a word. Clearing - // the pin under that flickers the border off again. + // the pin under that flickers the border off again. A cell in an edited + // sheet holds its pin too, because the editor opens over that cell and + // reads it. A header opens none, so it still clears. if (cell === pinnedCell) { - if (event.detail <= 1) { + var takenByEditor = editingEnabled() && cell.tagName === "TD"; + if (event.detail <= 1 && !takenByEditor) { pin(-1, null, null); } return; diff --git a/test/browser/sheet/editing.html b/test/browser/sheet/editing.html index f83729dc6..743f67b5d 100644 --- a/test/browser/sheet/editing.html +++ b/test/browser/sheet/editing.html @@ -372,6 +372,52 @@ ops().length === 0 && last().dirty === false ); + + // What a tap does. `tapEdits` reads `(pointer: coarse)`, so the check + // answers that query the way a phone does and puts the real one back. + var realMatchMedia = window.matchMedia; + function pointer(kind) { + window.matchMedia = function (query) { + return query === "(pointer: coarse)" + ? { matches: kind === "coarse" } + : realMatchMedia.call(window, query); + }; + } + + check("the mode is still on", odr.editing.isEnabled() === true); + pointer("fine"); + odr.sheet.pin(null); + click(cell(0, 0)); + check("a fine pointer selects a cell rather than opening it", editor() === null); + doubleClick(cell(0, 0)); + check("and opens it on the double click", editor() !== null); + press(editor(), "Escape"); + + pointer("coarse"); + click(cell(0, 0)); + check( + "a coarse pointer opens it on one click", + editor() !== null && editor().value === "a string that runs on" + ); + press(editor(), "Escape"); + + check("the locked cell is the one this uses", odr.editing.lockAt(1, 2) === "formula"); + click(cell(1, 2)); + check("a locked cell refuses the tap rather than opening", editor() === null); + + // A tap, a pause, a tap is two clicks of `detail: 1`, so the second one + // reaches the cell the first pinned. + pointer("fine"); + odr.sheet.pin({ column: 0, row: 0 }); + click(cell(0, 0)); + check("a second click holds the pin while the sheet is edited", odr.sheet.pinned() !== null); + var header = document.querySelectorAll(".odr-sheet-column-header")[1]; + click(header); + check("a header takes the pin", odr.sheet.pinned().column === 1); + click(header); + check("and a second click on it still clears the pin", odr.sheet.pinned() === null); + + window.matchMedia = realMatchMedia;