Skip to content

fix(ui): restore plan-reminder menu focus before opening the edit dialog - #1723

Merged
jackwener merged 1 commit into
mainfrom
fix/ui-plan-reminder-menu-focus-restore
Jul 31, 2026
Merged

fix(ui): restore plan-reminder menu focus before opening the edit dialog#1723
jackwener merged 1 commit into
mainfrom
fix/ui-plan-reminder-menu-focus-restore

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

The plan-reminder edit/duplicate flow defers opening the Astryx form dialog by two requestAnimationFrames after the row menu closes: the menu stashes an intent, runs it on close, then openReminderDialog remounts a closed form session before Astryx observes a false→true transition. Astryx Dialog restores focus on Escape to whatever was document.activeElement when it opened, and the menu's own close-focus-return sits in that same window. On a loaded CI runner the captured element can land on <body> instead of the menu trigger, so Escape strands focus away from the row and expect(menu).toBeFocused() times out.

This made the plan-reminders E2E flaky: on docs-only PR #1719 the same code tree (b1a0004e) passed on #1714 and failed on #1719, and a rerun went green. The suite runs with retries: 0, so the flake blocks unrelated PRs.

Keep a ref to each row's menu trigger (via the Astryx DropdownMenu button.ref) and re-focus it inside the deferred intent, right before the dialog opens, so the dialog captures and later restores focus to the row's trigger deterministically — independent of the menu-close vs deferred-open micro-timing.

Verification

  • packages/ui typecheck + @maka/ui build (tsc): clean
  • @maka/ui contract tests: 296 passed, 0 failed
  • biome lint packages/ui/src/plan-reminder-panel.tsx: clean
  • plan-reminders E2E (apps/desktop), --repeat-each 6: 12/12 passed, including closes a reminder menu before opening its edit dialog (the previously flaky case)
  • CI on this branch: typecheck and test green; e2e — the two plan-reminders tests pass, but 4 e2e/first-run.spec.ts tests fail. Those 4 fail with an identical pattern (4 failed, 93 passed) on at least three unrelated branches at the same time (codex/remove-expert-mode, feat/opencode-free-default-models, chore/prune-static-contract-tests), so they are a pre-existing active flaky cluster in the first-run boot flow, not a regression from this change (which only touches packages/ui/src/plan-reminder-panel.tsx).

Root cause

apps/desktop/e2e/plan-reminders.spec.ts:75 › closes a reminder menu before opening its edit dialog asserted expect(menu).toBeFocused() after Escape closed the edit dialog. Astryx Dialog restores focus to the element it captured as document.activeElement at open time (triggerElementRef.current = document.activeElement in its open effect). The edit flow deferred the dialog open two rAFs past the menu close, so the menu's close-focus-return and the dialog's capture raced; under CI load the capture landed on <body>, so Escape restored to <body> and the trigger stayed inactive for the full 10s timeout.

The plan-reminder edit/duplicate flow defers opening the Astryx form dialog
by two requestAnimationFrames after the row menu closes (the menu stashes an
intent, runs it on close, then remounts a closed form session before Astryx
observes a false->true transition). Astryx Dialog restores focus on Escape
to whatever was `document.activeElement` when it opened, and the menu's own
close-focus-return sits in that same window. On a loaded CI runner the
captured element can land on <body> instead of the menu trigger, so Escape
strands focus away from the row and `expect(menu).toBeFocused()` times out.

This made the `plan-reminders` E2E flaky on docs-only PR #1719 (same code
tree passed on #1714 and failed on #1719; a rerun went green), and the
suite runs with retries: 0 so the flake blocks unrelated PRs.

Keep a ref to each row's menu trigger and re-focus it inside the deferred
intent, right before the dialog opens, so the dialog captures and later
restores focus to the row's trigger deterministically — independent of the
menu-close vs deferred-open micro-timing.

Verified: packages/ui typecheck + 296 contract tests, biome lint, and the
plan-reminders E2E (12/12 with --repeat-each 6).
@jackwener
jackwener merged commit ec7b41d into main Jul 31, 2026
4 of 6 checks passed
@jackwener
jackwener deleted the fix/ui-plan-reminder-menu-focus-restore branch July 31, 2026 18:21
Astro-Han added a commit that referenced this pull request Jul 31, 2026
#1720 (opencode-free as a zero-credential default provider) added
`ensureBootstrapConnection`, which unconditionally seeds an `opencode-free`
connection on first launch when no connections exist. The E2E gate that
skips it, `if (!e2eFixture)`, only triggers when `MAKA_E2E_FIXTURE` is set,
but the `emptyWindow` fixture (used by all four `first-run.spec.ts` tests)
passes `seed: false` and no `e2eFixtureScenario`, so `MAKA_E2E_FIXTURE` is
unset and the bootstrap runs — seeding `opencode-free` into the "empty"
workspace. The onboarding state becomes `ready_empty` instead of
`needs_connection`, so `OnboardingHero` no longer renders
`.maka-firstrun-row`, and the four tests fail with `toHaveCount 0` / click
timeouts.

This is a deterministic regression on current main (not a flake): #1720's
own e2e failed exactly these four; every post-#1720 run fails them
identically; pre-#1720 runs passed. #1720 merged with the failing e2e, so
every PR's e2e is now red on these four.

The `needs_connection` first-run hero is now unreachable for a fresh
install (opencode-free is always seeded, and re-seeded whenever the
connection list is empty), so these tests assert a contract that no longer
holds. Drop them now to unblock e2e; follow-up will add coverage for the
post-#1720 fresh-install boot contract (opencode-free seeded -> ready_empty).
Also remove the now-unused `emptyWindow` fixture (it was "Used by first-run
only").

Unblocks the plan-reminder focus-restore fix (#1723) and the docs PR #1719,
whose e2e runs merge with main and so also hit this cluster.

Verified: `npx playwright test --list` parses the remaining 93 tests across
32 files with no import/syntax errors; `emptyWindow` has no remaining
references. The 93 remaining tests passed in the CI runs that failed only
on these four.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants