chrome.js hears Escape in the capture phase
Sheets' menubar has to stop keys propagating out of its menus (the grid library acts on every key it hears on document), so a bubble-phase Escape listener on document never ran there and an open menu stayed open. Outside-click already listened in capture; Escape now does too. Docs' menubar closes its own menus on Escape and finds nothing open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+18
−3
chrome/chrome.js+10 −3chrome/chrome_test.go+8 −0
CI failed — run details
CI log
=== vet === CGO_ENABLED=0 go vet ./... ../../cache/go-mod/github.com/carlosframework/rastrillo@v0.23.0/sessions/sessions.go:72:12: pattern migrations/*.sql: no matching files found make: *** [Makefile:22: test-vet] Error 1 amadan: step vet failed: exit status 2 amadan: job failed at step vet — its output is above, not at the tail of this log
diff --git a/chrome/chrome.js b/chrome/chrome.js| index 0ef7ea3..15a9333 100644 |
| --- a/chrome/chrome.js |
| +++ b/chrome/chrome.js |
| @@ -16,8 +16,15 @@ |
| // |
| // The group is one group in every app, so closing it is estilo's job, not |
| // each app's: Sheets' menubar lost Escape and outside-click entirely when |
| -// it left rastrillo's dropdown. An app script that handles a key first |
| -// (Docs' menubar) calls preventDefault, and this then does nothing. |
| +// it left rastrillo's dropdown. |
| +// |
| +// Both listen in the capture phase. A document app has to stop key |
| +// events from propagating out of its menus — Sheets' grid library acts on |
| +// every key it hears on document, so an arrow in an open menu would walk |
| +// the cell cursor too — and a bubble-phase listener on document never |
| +// hears the Escape such an app stops. Capture hears it first. An app that |
| +// closes its own menu on Escape (Docs' menubar) finds nothing open and |
| +// does nothing, so nothing is handled twice. |
| ;(() => { |
| const group = 'details[name="estilo-menus"][open]' |
| const openPanel = () => document.querySelector(group) |
| @@ -37,7 +44,7 @@ |
| const summary = open.querySelector('summary') |
| if (summary) summary.focus() |
| e.preventDefault() |
| - }) |
| + }, true) |
| document.addEventListener('click', (e) => { |
| const open = openPanel() |
diff --git a/chrome/chrome_test.go b/chrome/chrome_test.go| index 052f7d8..0847309 100644 |
| --- a/chrome/chrome_test.go |
| +++ b/chrome/chrome_test.go |
| @@ -429,4 +429,12 @@ func TestScriptIsClassicAndSelfContained(t *testing.T) { |
| t.Errorf("chrome.js lost %q", want) |
| } |
| } |
| + // Escape is heard in the capture phase: an app that stops a key from |
| + // propagating out of its menus (Sheets, whose grid library listens on |
| + // document) would otherwise swallow it before a bubble listener ran. |
| + keydown := src[strings.Index(src, "addEventListener('keydown'"):] |
| + keydown = keydown[:strings.Index(keydown[1:], "document.addEventListener(")+1] |
| + if !strings.HasSuffix(strings.TrimSpace(keydown), "}, true)") { |
| + t.Error("chrome.js's Escape handler is not in the capture phase") |
| + } |
| } |