From cf8e01c31b41d38d6826ddb230fffe0c7a136180 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 20 Sep 2026 11:47:10 +0200 Subject: [PATCH 1/2] feat(html): the viewer states whether a click opens a sheet cell The pointer decides whether a click opens a cell's editor, and it is a guess: an android WebView reports `(pointer: coarse)` as false on an emulator, so the tap-to-edit of #908 did not happen there at all. `odr.editing.setSheetOptions({editOnClick})` lets the viewer state it, as `odr.annotation.setOptions` states the marking gestures. Unstated, the pointer answers as before. Checked with `test/browser/sheet` - 77 checks on the editing page, none failing, seven of them new. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FJJdfqpnVCKBNHXjAVxSou --- CHANGELOG.md | 5 +++ .../internal/html/frontend/sheet-editing.js | 33 ++++++++++++-- test/browser/sheet/editing.html | 44 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca30cd19..2dada81e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ The release run heads these entries with the version and opens a fresh four screens wide, which the browser's own floor of 0.25 cannot reach. An A0 pdf page now zooms out to fit; narrower content is unchanged. +- `odr.editing.setSheetOptions({editOnClick})` states whether a click opens a + sheet cell's editor, for a viewer that knows its input better than the + pointer does - an android WebView on an emulator reports a fine pointer on a + touch screen. Unstated, the pointer answers as before. + - **Fix**: selecting text in a pdf view showed the invisible layer that carries it, as a second set of glyphs over the drawn ones. That layer now stays transparent under `::selection`, and states the highlight background, which diff --git a/src/odr/internal/html/frontend/sheet-editing.js b/src/odr/internal/html/frontend/sheet-editing.js index 89e69731a..40892b0a5 100644 --- a/src/odr/internal/html/frontend/sheet-editing.js +++ b/src/odr/internal/html/frontend/sheet-editing.js @@ -405,16 +405,43 @@ } }); - /// 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. + /// Gesture policy, the viewer's to set. `editOnClick` null asks the pointer. + var options = { editOnClick: null }; + + /// Whether a click opens the editor, where a double click always does. + /// + /// A touch screen has no double click to spare: the first tap of one is a + /// tap of its own, and the reader is already in the mode that edits. The + /// pointer is the fallback answer only, because it is a guess - an android + /// WebView on an emulator reports a fine one - and the viewer knows. function tapEdits() { + if (options.editOnClick !== null) { + return !!options.editOnClick; + } return ( typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches ); } + /// Merged into what is set; an unknown key throws. + odr.editing.setSheetOptions = function (value) { + Object.keys(value || {}).forEach(function (key) { + if (!Object.prototype.hasOwnProperty.call(options, key)) { + throw new Error("odr.editing: unknown sheet option " + key); + } + options[key] = value[key]; + }); + }; + + odr.editing.getSheetOptions = function () { + var copy = {}; + Object.keys(options).forEach(function (key) { + copy[key] = options[key]; + }); + return copy; + }; + // A locked cell says so on the click, not on the double click. table.addEventListener("click", function (event) { var at = diff --git a/test/browser/sheet/editing.html b/test/browser/sheet/editing.html index 743f67b5d..4be28ff1a 100644 --- a/test/browser/sheet/editing.html +++ b/test/browser/sheet/editing.html @@ -361,6 +361,50 @@ odr.editing.committed(); check("and a save clears them with the log", staleNow() === ""); + // --------------------------------------- who says a tap opens the cell + + // The pointer is a guess: an android WebView on an emulator reports a + // fine one. So the viewer may state it outright. + function clickCell(column, row) { + odr.editing.disable(); + odr.editing.enable(); + cell(column, row).dispatchEvent( + new MouseEvent("click", { bubbles: true, detail: 1 }) + ); + } + + check( + "the pointer answers until the viewer states it", + odr.editing.getSheetOptions().editOnClick === null + ); + + odr.editing.setSheetOptions({ editOnClick: false }); + clickCell(2, 2); + check("stated off, one click opens nothing", editor() === null); + cell(2, 2).dispatchEvent(new MouseEvent("dblclick", { bubbles: true, detail: 2 })); + check("and the double click still opens the cell", editor() !== null); + + odr.editing.setSheetOptions({ editOnClick: true }); + clickCell(0, 0); + check("stated on, one click opens it", editor() !== null); + check("holding what the cell shows", editor().value === "a string that runs on"); + check( + "and the cell is pinned under it", + cell(0, 0).classList.contains("odr-sheet-pinned-cell") + ); + + var threw = false; + try { + odr.editing.setSheetOptions({ notAnOption: true }); + } catch (error) { + threw = true; + } + check("an unknown option throws", threw); + + odr.editing.setSheetOptions({ editOnClick: null }); + odr.editing.disable(); + odr.editing.enable(); + odr.editing.editAt(3, 0); editor().value = "one"; press(editor(), "Enter"); From 8e6041336727116c74b57a07db376adab6f72cda Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 20 Sep 2026 11:56:59 +0200 Subject: [PATCH 2/2] fix(html): an unset `editOnClick` asks the pointer rather than reading as off `setSheetOptions({editOnClick: undefined})` set the key, so `!== null` took it as a stated no and a tap opened nothing. `!= null` reads both null and undefined as unstated, which is what a host passing a value it does not have means. The design doc said a double click is what opens the editor, which #908 and this one no longer make true. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0149gFxhkvKTBQidz6brU8Vt --- CHANGELOG.md | 5 ++--- docs/design/spreadsheet-editing.md | 3 ++- .../internal/html/frontend/sheet-editing.js | 15 +++++++------- test/browser/sheet/editing.html | 20 +++++++++++++++++++ 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dada81e8..f316a7c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,8 @@ The release run heads these entries with the version and opens a fresh pdf page now zooms out to fit; narrower content is unchanged. - `odr.editing.setSheetOptions({editOnClick})` states whether a click opens a - sheet cell's editor, for a viewer that knows its input better than the - pointer does - an android WebView on an emulator reports a fine pointer on a - touch screen. Unstated, the pointer answers as before. + sheet cell's editor, because the pointer is a guess: an android WebView + reports a fine one on a touch screen. Unstated, the pointer answers. - **Fix**: selecting text in a pdf view showed the invisible layer that carries it, as a second set of glyphs over the drawn ones. That layer now stays diff --git a/docs/design/spreadsheet-editing.md b/docs/design/spreadsheet-editing.md index 34f0a10ba..96b1c6e00 100644 --- a/docs/design/spreadsheet-editing.md +++ b/docs/design/spreadsheet-editing.md @@ -388,7 +388,8 @@ Each step ships on its own. "Both" means `.ods` and `.xlsx`. with their code table (decision 7). `spreadsheet_js` publishes `odr.sheet` in the same step (decision 8) — the position map the mode reads a lock through. 2. **Landed**, with item 3: an editor that drops what is typed is not one. - Overlay editor: double-click / Enter / typing opens it over the cell; Enter, + Overlay editor: double-click / Enter / typing opens it over the cell, and a + single click where `editOnClick` or the pointer says so; Enter, Tab and blur commit; Escape cancels; arrow keys move the pin, through `odr.sheet.pin` rather than a pin of its own. A locked cell refuses on the click rather than on the double click that would have opened it. diff --git a/src/odr/internal/html/frontend/sheet-editing.js b/src/odr/internal/html/frontend/sheet-editing.js index 40892b0a5..feaf7bdf7 100644 --- a/src/odr/internal/html/frontend/sheet-editing.js +++ b/src/odr/internal/html/frontend/sheet-editing.js @@ -405,17 +405,16 @@ } }); - /// Gesture policy, the viewer's to set. `editOnClick` null asks the pointer. + /// Gesture policy, the viewer's to set. `editOnClick` unstated asks the + /// pointer. var options = { editOnClick: null }; - /// Whether a click opens the editor, where a double click always does. - /// - /// A touch screen has no double click to spare: the first tap of one is a - /// tap of its own, and the reader is already in the mode that edits. The - /// pointer is the fallback answer only, because it is a guess - an android - /// WebView on an emulator reports a fine one - and the viewer knows. + /// Whether a click opens the editor, where a double click always does. The + /// pointer answers only while `editOnClick` is unstated, because it is a + /// guess: an android WebView reports a fine one on a touch screen. function tapEdits() { - if (options.editOnClick !== null) { + // `!= null` so an unset key a host passes reads as unstated, not as off + if (options.editOnClick != null) { return !!options.editOnClick; } return ( diff --git a/test/browser/sheet/editing.html b/test/browser/sheet/editing.html index 4be28ff1a..55724655b 100644 --- a/test/browser/sheet/editing.html +++ b/test/browser/sheet/editing.html @@ -365,6 +365,14 @@ // The pointer is a guess: an android WebView on an emulator reports a // fine one. So the viewer may state it outright. + // what `tapEdits` answers, read through the one gesture that shows it + function tapEditsNow() { + clickCell(0, 0); + var open = editor() !== null; + odr.editing.disable(); + odr.editing.enable(); + return open; + } function clickCell(column, row) { odr.editing.disable(); odr.editing.enable(); @@ -393,6 +401,18 @@ cell(0, 0).classList.contains("odr-sheet-pinned-cell") ); + // A host passing a key it has no value for means unstated, not off. The + // pointer has to say coarse for the two to tell apart. + var realMM = window.matchMedia; + window.matchMedia = function (query) { + return query === "(pointer: coarse)" + ? { matches: true } + : realMM.call(window, query); + }; + odr.editing.setSheetOptions({ editOnClick: undefined }); + check("a key passed unset asks the pointer rather than reading as off", tapEditsNow()); + window.matchMedia = realMM; + var threw = false; try { odr.editing.setSheetOptions({ notAnOption: true });