oficina / estilo Public

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>
Paul Campbell pushed by paul@keymail.dev 90f93a37e7a0a74e971708e672267724622fbd08 parent 50567b1
2 files changed, +18 −3
  • chrome/chrome.js +10 −3
  • chrome/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")
+ }
}