Skip to content

refactor(ui): migrate renderer modals onto Base UI Dialog (#520) - #548

Merged
Astro-Han merged 5 commits into
mainfrom
feat/base-ui-large-migrations
Jul 5, 2026
Merged

Astro-Han merged 5 commits into
mainfrom
feat/base-ui-large-migrations

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Converge every hand-written renderer modal onto Base UI Dialog so focus trap, Esc, restore-focus, and aria-hidden come from one shell instead of the self-rolled useModalA11y + per-site role="dialog"/aria-modal + the 120-line useProviderSheetBackgroundInert DOM walker. Three commits, each an independent revert boundary.

Why

Refs #520 (PR7 dialog/drawer slice). Five modal surfaces were still hand-writing dialog semantics on top of useModalA11y, and the three provider sub-sheets additionally ran a hand-rolled DOM walker (useProviderSheetBackgroundInert) to simulate nested-modal background inert. Base UI Dialog already does all of this — keeping the hand-rolled versions is duplicated maintenance and an a11y regression risk.

Scope

Changed (commit 1 → 2 → 3):

  • commit 1 d5d0a9d2 — KeyboardHelpModal + CommandPalette hand-written role="dialog" + useModalA11y → DialogRoot/DialogContent; SearchModal drops residual useModalA11y in favor of DialogContent initialFocus/finalFocus. Adds dialog-source-contract banning hand-written role="dialog"/aria-modal outside a Drawer-migration allowlist.
  • commit 2 d0413022 — WeChatScanLoginModal + WechatQrLoginModal → DialogContent; drops orphan settingsModalBackdrop + settingsWechatQrBackdrop CSS.
  • commit 3 fee54b12 — ProviderConfigSheetOverlay + ClaudeSubscriptionModal + SubscriptionLoginModal → Base UI Dialog nested + Dialog.Portal container=.settingsSurface so the scrim stays inside the settings surface (nested visual preserved). Dialog.Backdrop forceRender because Base UI skips nested backdrops by default. Retires the last useModalA11y consumers → deletes modal-a11y.ts + components.tsx export + the useProviderSheetBackgroundInert walker. Allowlist now empty.

Not included (PR7 remainder, separate PRs):

Verification

  • Contracts: dialog-source, modal-lifecycle, command-palette-a11y-copy, search-modal-lifecycle, renderer-utility-primitives, model-oauth-section, settings-form-a11y, ui-tsx-dead-export, renderer-style-pruning, app-region-hygiene all pass.
  • npm run -w @maka/desktop test: 2019/2019. npm run typecheck: clean.
  • Screenshots vs fresh cc3fc2cd baseline: sidebar-search-modal-open 0.05%, command-palette-open 0.05%, settings-bots ~0, settings-data ~0.
  • commit 3 CDP probe (storybook + electron) on the real ProviderConfigSheetOverlay inside a .settingsSurface: backdrop renders (absolute inset 0, providerConfigOverlay CSS), sheet portals into the surface, nav/main get aria-hidden="true" + data-base-ui-inert, sheet gets role="dialog". Probe scripts were local-only and deleted.

User-facing impact

One behavior change in commit 3: clicking the settings nav area while a provider sheet is open now closes the sheet (Base UI dismiss-on-backdrop-click) instead of being silently swallowed by the old inert. This matches PermissionDialog and other standard modals (click outside = close).

Reviewer notes

  • commit 3 is the non-trivial one. Two Base UI specifics make the nested side-sheet work: Dialog.Portal container (nested visual) and Dialog.Backdrop forceRender (Base UI skips nested backdrops by default). Both are explained in the ProviderSheet doc comment and verified by the CDP probe.
  • The behavior change (click outside = close) is intentional and called out above; push back if you want the old swallow-on-click semantics preserved.

Astro-Han added 3 commits July 5, 2026 18:31
…I Dialog (#520 PR7)

Migrate the two remaining hand-written top-level modals onto Base UI
DialogRoot/DialogContent so focus trap, Esc, and focus restoration come from
the same shell SearchModal/PermissionDialog already use. SearchModal already
rendered via DialogContent but still called useModalA11y for initial focus +
suppressFocusRestore; replace with DialogContent initialFocus={inputRef} and a
finalFocus callback that returns false when navigating to a result so the
destination owns focus.

- keyboard-help.tsx, command-palette.tsx: hand-written role="dialog" +
  aria-modal + useModalA11y -> DialogRoot/DialogContent
- search-modal.tsx: drop residual useModalA11y; DialogContent initialFocus/
  finalFocus instead
- dialog-source-contract.test.ts (new): ban hand-written role="dialog" /
  aria-modal outside the Drawer-migration allowlist (provider-config-sheet,
  provider-oauth-section, bot-chat-settings-page move to Base UI Drawer in a
  follow-up commit)
- drop orphan maka-palette-backdrop CSS (positioning now via DialogContent
  top-[12vh] className)
- update contracts that previously asserted on useModalA11y

Verification: dialog-source + modal-lifecycle + command-palette-a11y +
search-modal-lifecycle + renderer-utility contracts 51/51; desktop 2020/2020;
typecheck clean; sidebar-search-modal-open + command-palette-open screenshots
vs fresh cc3fc2c baseline 0.05% residual.
…og (#520 PR7)

Migrate WeChatScanLoginModal + WechatQrLoginModal in bot-chat-settings-page
from hand-written role="dialog" + aria-modal + useModalA11y onto Base UI
DialogRoot/DialogContent (same shell as commit 1). Drop the now-orphan
settingsModalBackdrop + settingsWechatQrBackdrop CSS rules (backdrop now
comes from DialogContent). Remove bot-chat-settings-page from the
dialog-source-contract Drawer-migration allowlist (only provider-config-sheet
+ provider-oauth-section remain, pending Base UI Drawer in the next commit).

Verification: dialog-source + settings-form-a11y + model-oauth-section +
modal-lifecycle + renderer-style-pruning + app-region-hygiene contracts pass;
desktop 2020/2020; typecheck clean; settings-bots + settings-general
screenshots vs fresh cc3fc2c baseline 0 diff.
…ortal container (#520 PR7)

Migrate ProviderConfigSheetOverlay + ClaudeSubscriptionModal +
SubscriptionLoginModal from hand-written role="dialog" + useModalA11y +
the 120-line useProviderSheetBackgroundInert DOM walker onto Base UI Dialog.
Dialog.Portal container=.settingsSurface keeps the backdrop + popup INSIDE
the settings surface (nested visual preserved: scrim covers only the surface,
rounded corners stay aligned to the modal).

Base UI's modal layer now owns focus trap, Esc, and aria-hidden on the
settings nav/list. Dialog.Backdrop forceRender because Base UI skips nested
backdrops by default; provider sheets are side panels that want the scrim.

This retires the last useModalA11y consumers, so modal-a11y.ts +
components.tsx's useModalA11y export are deleted, and the
dialog-source-contract Drawer-migration allowlist is now empty.

Verified via CDP probe (storybook + electron) on the real
ProviderConfigSheetOverlay inside a .settingsSurface: backdrop renders
(absolute inset 0, providerConfigOverlay CSS), sheet is portal'd into the
surface, nav/main get aria-hidden="true" + data-base-ui-inert, sheet gets
role="dialog".

Behavior change: clicking the settings nav area while a sheet is open now
closes the sheet (Base UI dismiss on backdrop click) instead of being silently
swallowed by the old inert — matches PermissionDialog and standard modals.

Verification: dialog-source + model-oauth + settings-form-a11y + dead-export
+ search-modal-lifecycle contracts pass; desktop 2019/2019; typecheck clean;
settings-bots + settings-data screenshots vs fresh cc3fc2c baseline ~0 diff.
Astro-Han added 2 commits July 5, 2026 20:37
…form

- KeyboardHelpModal + WeChatScanLoginModal + WechatQrLoginModal: pass
  showClose={false} to DialogContent. These three keep their own in-header
  close button (with a specific aria-label like "关闭快捷键面板"), and
  DialogContent's default showClose was rendering a duplicate. Provider sheets
  are unaffected — they use BaseDialog.Popup, not DialogContent/createModalContent.
- dialog-source-contract: FORBIDDEN_PATTERNS now matches the JSX expression
  form (role={'dialog'}, role={"dialog"}, role={`dialog`}, same for
  aria-modal) in addition to the literal form, with a coverage test that
  asserts both positive and benign-negative samples.
- ProviderSheet: document the singleton assumption on the `.settingsSurface`
  querySelector (SettingsModal is conditional-mounted, at most one surface).

Two review items intentionally not changed (reasoning in PR review reply):
querySelector owner-ref (YAGNI — .settingsSurface is a singleton today) and
the StrictMode focus-restore behavior test (the commit-1 static finalFocus
test already fails if the false branch is removed).
…label

- dialog-source-contract: FORBIDDEN_PATTERNS regex now tolerates spaces
  around `=` and inside `{}`, and matches `aria-modal={true}` (bare boolean,
  no quotes) in addition to the quoted form. Positives add `aria-modal={true}`,
  `aria-modal = { true }`, `role = {'dialog'}`, `role = "dialog"`. Still regex,
  no AST — same rationale as the P2 fix.
- WeChatScanLoginModal: in-header close aria-label "关闭" → "关闭微信扫码登录".
  The other two retained close buttons already had specific labels
  (关闭快捷键面板, 关闭微信扫码登录), so all three internal close buttons now
  carry concrete copy.
@Astro-Han
Astro-Han merged commit 0b96656 into main Jul 5, 2026
@Astro-Han
Astro-Han deleted the feat/base-ui-large-migrations branch July 5, 2026 13:03
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.

1 participant