fix(editor): Escape closes only the topmost layer; menus close when focus leaves #167

Merged
dominik.polakovics merged 1 commit from afk/140 into main 2026-10-08 15:54:10 +02:00

Closes #140

What

New web/src/layers.ts: one stack of open layers that decides what an Escape, an outside press or a focus change closes. Every dialog, every menu and the Media details panel is on it while open.

  • Escape closes the topmost layer only, wherever focus is (the page body included). Escapes that are isComposing or already preventDefault()ed close nothing, so the page tree's inline rename and new-page inputs still use Escape themselves.
  • Menus also close on a press outside the menu and its button, when focus moves outside them, and on window blur (a click into the preview frame). So at most one menu is open at a time. Escape returns focus to the menu's button; the rich-text menus leave focus alone.
  • Dialogs and the details panel close on Escape only (dialogs keep their backdrop press, focus handling and Tab trap).
  • Layers are independent, and a layer whose component unmounts leaves the stack. useLayer memoizes open, so a signal changing under an open layer (e.g. showing another file in the details panel) doesn't move the layer back on top.
  • No listeners while nothing is open. The module listens on window in the bubble phase, after Solid's delegated document handlers; its header explains why stopPropagation() can't order handlers under delegation.

Moved onto it, with their own Escape/outside-press handling deleted: Dialog, Media PopMenu and details panel, page tree row menu, page editor header menu, element ⋮ menu, header host-navigation menus, rich-text Insert/Table menus.

Behaviour changes in Media: a press on a tile body now closes an open tile menu, because only the menu and its ⋯ button count as inside. A right-click still reopens the menu at the cursor.

Tests

  • src/test/layers.test.ts: unit tests for the module, including the listener add/remove set.
  • New cases in MediaModule, ElementCrud, PageManage, PageActions and rte/tableMenu for: Escape closing only the topmost layer, focus after Escape, focus leaving, window blur, and the two-element-menus case. Most of these fail on main.
  • HostNav tests that pinned the capture-phase keydown and the menus' own listener set now pin behaviour instead.

npm run build, tsc --noEmit, npm test (2036 passing), go vet ./... and go test ./... all pass.

scaffold/site/AGENTS.md and README are unchanged: nothing they document changed.

🤖 Generated with Claude Code

Closes #140 ## What New `web/src/layers.ts`: one stack of open layers that decides what an Escape, an outside press or a focus change closes. Every dialog, every menu and the Media details panel is on it while open. - **Escape** closes the topmost layer only, wherever focus is (the page body included). Escapes that are `isComposing` or already `preventDefault()`ed close nothing, so the page tree's inline rename and new-page inputs still use Escape themselves. - **Menus** also close on a press outside the menu and its button, when focus moves outside them, and on window blur (a click into the preview frame). So at most one menu is open at a time. Escape returns focus to the menu's button; the rich-text menus leave focus alone. - **Dialogs and the details panel** close on Escape only (dialogs keep their backdrop press, focus handling and Tab trap). - **Layers are independent**, and a layer whose component unmounts leaves the stack. `useLayer` memoizes `open`, so a signal changing under an open layer (e.g. showing another file in the details panel) doesn't move the layer back on top. - **No listeners while nothing is open.** The module listens on `window` in the bubble phase, after Solid's delegated `document` handlers; its header explains why `stopPropagation()` can't order handlers under delegation. Moved onto it, with their own Escape/outside-press handling deleted: `Dialog`, Media `PopMenu` and details panel, page tree row menu, page editor header menu, element ⋮ menu, header host-navigation menus, rich-text Insert/Table menus. Behaviour changes in Media: a press on a tile body now closes an open tile menu, because only the menu and its ⋯ button count as inside. A right-click still reopens the menu at the cursor. ## Tests - `src/test/layers.test.ts`: unit tests for the module, including the listener add/remove set. - New cases in MediaModule, ElementCrud, PageManage, PageActions and rte/tableMenu for: Escape closing only the topmost layer, focus after Escape, focus leaving, window blur, and the two-element-menus case. Most of these fail on `main`. - HostNav tests that pinned the capture-phase keydown and the menus' own listener set now pin behaviour instead. `npm run build`, `tsc --noEmit`, `npm test` (2036 passing), `go vet ./...` and `go test ./...` all pass. `scaffold/site/AGENTS.md` and README are unchanged: nothing they document changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(editor): Escape closes only the topmost layer; menus close when focus leaves
All checks were successful
ci / build-and-test (pull_request) Successful in 9m18s
596544a3bc
Add web/src/layers.ts, one stack of open layers (dialogs, menus, the Media
details panel). Escape closes the topmost layer only, wherever focus is;
an Escape that is composing or already preventDefault()ed closes nothing.
Menus also close on a press outside them and their button, when focus
leaves them, and when the window loses focus. The module listens on
window in the bubble phase, after Solid's delegated handlers, and only
while a layer is open.

Move Dialog, the Media menus and details panel, the page tree row menu,
the page editor header menu, the element ⋮ menu, the header
host-navigation menus and the rich-text Insert and Table menus onto it,
deleting their own Escape and outside-press handling. Escape returns
focus to a menu's button (the rich-text menus leave focus alone).

Closes #140

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

PASS. Validated for landing against issue #140.

  • Checks: Forgejo Actions ci / build-and-test is green on 596544a (web build, vitest, go vet/test/build); not re-run.
  • Conventions: Conventional Commits title; body carries Closes #140; head afk/140 is based on current main (no conflict).
  • Diff scope matches the issue's inventory: new web/src/layers.ts stack; Dialog, HostNav, Media PopMenu and details panel, page tree row menu, page editor header menu, element ⋮ menu and the rich-text Insert/Table menus moved onto it, with their own Escape / outside-press / focus listeners deleted. The only stopPropagation() left on key events are Dialog's Tab trap and the page tree's inline rename / new-page inputs, which the issue says keep their own Escape. README's header-menu paragraph still holds; AGENTS.md untouched, correctly.
  • Tests cover each acceptance criterion (layers unit tests incl. listener add/remove set; Media rename/delete/menu-over-panel cases; two-element-menus case; focus-leaving and window-blur per menu).

No CONCERNS.

[autoland] verdict: pass > *This was generated by AI while landing a PR.* **PASS.** Validated for landing against issue #140. - Checks: Forgejo Actions `ci / build-and-test` is green on 596544a (web build, vitest, go vet/test/build); not re-run. - Conventions: Conventional Commits title; body carries `Closes #140`; head `afk/140` is based on current `main` (no conflict). - Diff scope matches the issue's inventory: new `web/src/layers.ts` stack; Dialog, HostNav, Media PopMenu and details panel, page tree row menu, page editor header menu, element ⋮ menu and the rich-text Insert/Table menus moved onto it, with their own Escape / outside-press / focus listeners deleted. The only `stopPropagation()` left on key events are Dialog's Tab trap and the page tree's inline rename / new-page inputs, which the issue says keep their own Escape. README's header-menu paragraph still holds; AGENTS.md untouched, correctly. - Tests cover each acceptance criterion (layers unit tests incl. listener add/remove set; Media rename/delete/menu-over-panel cases; two-element-menus case; focus-leaving and window-blur per menu). No CONCERNS.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!167
No description provided.