Skip to content

Extension modal open beside the first-launch wizard locks the wizard; the wizard does not hold keyboard focus #3837

Closed

Description

@mstomar125

Summary

On a first launch core shows its first-launch setup wizard (components/onboarding/onboarding-wizard.svelte, mounted by components/layout/app-shell.svelte). An extension that opens a dialog from the app-shell-modal chrome slot at the same moment (one extension app does: its sign-in prompt opens on the daemon's first signed-out report, seconds after launch) makes the wizard unusable:

  • core's dialog ($lib/components/ui/dialog, bits-ui 1.8 Dialog, which the host API exports as Dialog from @nodespace/extension-api/ui) is modal: while it is open its scroll lock sets pointer-events: none on <body> and its focus scope holds focus inside the dialog;
  • the wizard is a hand-rolled overlay (.onboarding-backdrop, position: fixed, z-index: 200) that sets no pointer-events of its own, so it inherits none from the body and takes no click;
  • the wizard sits inside ThemeProvider (.theme-provider { position: relative; z-index: 0 }, its own stacking context), while the dialog is portalled to <body> at z-50, so the dialog and its overlay render on top of the wizard.

Measured in Chromium with core's real ThemeProvider and OnboardingWizard at the release pin and the extension's dialog open:

  • getComputedStyle(document.body).pointerEvents = none;
  • document.elementFromPoint at the centre of the wizard's Next (and of its close button) = the dialog's overlay ([data-slot=dialog-overlay]);
  • a click on Next lands on the overlay, which bits-ui treats as an outside click: it closes the extension's dialog (as a dismissal) and the wizard does not advance; only a second click reaches the wizard;
  • Escape goes to the dialog (its focus scope holds focus), not to the wizard.

The same happens in the other order (the extension's dialog first, the wizard opened over it), and with any other core modal shown while an extension dialog is open.

A second, core-only finding: the wizard does not hold keyboard focus

In core's real built app (Chromium, core's frontend at the release pin built with the Sync extension, none of whose dialogs is open, and a Tauri IPC mock answering core's commands as on a fresh install), document.activeElement while the wizard is open is the journal's textarea.node__content behind it, measured at 0, 0.3, 1 and 2 s after the wizard appears. The wizard's focusTrap focuses its first control on mount, then the daily-journal editor's autofocus takes focus back. Consequences:

  • Escape does nothing: the trap's key handler sits on the wizard element, and focus is outside it.
  • Tab, and typing, go to the journal behind the modal (text lands in today's note).
  • The first real install test shows the same thing: the screenshot of the wizard has the text caret blinking in the empty journal node behind it.

Suggested fix: suspend the editor's autofocus while the onboarding wizard (or any modal) is open, or have focusTrap take focus back on focusin outside the trap, as bits-ui's focus scope does.

Workaround in the extension (shipped there)

The extension's chrome dialogs now take a turn: they open only while no other aria-modal="true" element is on screen, step aside (closing without recording an answer) when one appears, and come back when it is gone. Core gives extensions no signal for its own modals, so this watches the DOM with a MutationObserver while a Sync dialog is waiting or shown.

Suggested core fixes

  1. Make the wizard robust to a body pointer lock: pointer-events: auto on .onboarding-backdrop (bits-ui sets the same on its own overlay and content for exactly this reason). The other hand-rolled modals with the same shape (delete-confirmation-modal.svelte, subtree-access-denied-modal.svelte) have the same exposure.
  2. Give extensions a signal: expose "a core modal is open" (or "onboarding is open") through @nodespace/extension-api, for example a reactive shell.modalOpen getter or a when()-usable predicate, so a chrome contribution can wait for core's modals without watching the DOM.
  3. Optionally, document in the extension points guide that an app-shell-modal contribution shares the screen with core's own modals (the first-launch wizard is not in that slot) and should not open while one is shown.

Reproduction

The extension's browser harness mounts core's real ThemeProvider and OnboardingWizard with the Sync chrome (scripts/ui-browser/wizard-shell.svelte, page ?shell&wizard, bun run test:ui-browser, describe block "next to core's first-launch wizard"). Reverting the extension-side turn-taking makes those journeys fail at the wizard's Next.

Activity

  1. self-assigned this
    on Oct 16, 2026
  2. mstomar125 commented on Oct 20, 2026

    @mstomar125
    ContributorAuthor

    Planned for core v0.3.9.

  3. mstomar125 commented on Oct 20, 2026

    @mstomar125
    ContributorAuthor

    Partly fixed by #3855 (merged): the wizard now holds keyboard focus and the hand-rolled modals tolerate a body pointer lock. Still open here: the extension dialog stacking above the wizard (the Sync extension works around it by taking turns), the extension-API modal signal (fix 2) and the docs note (fix 3).

  4. mstomar125 commented on Oct 22, 2026

    @mstomar125
    ContributorAuthor

    Picking up the remaining parts: the extension-API signal for core modals (fix 2) and the extension-points docs note (fix 3).

  5. mstomar125 commented on Oct 22, 2026

    @mstomar125
    ContributorAuthor

    Done: #3865 (merged, API addition acked by the architect) adds shell.modalOpen to @nodespace/extension-api; core's wizard, delete confirmation and access-denied dialog report through it. Guide text is in nodespace-docs PR 132 for review. The Sync extension can now replace its DOM watcher with the signal. bits-ui dialogs do not register yet (they are user-initiated, not startup).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions