Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) by Astro-Han · Pull Request #542 · apache/maka · GitHub
Skip to content

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6) - #542

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog
Jul 5, 2026
Merged

refactor(ui): migrate toast + confirm + permission dialogs to Base UI (#520 PR6)#542
Astro-Han merged 7 commits into
mainfrom
refactor/toast-alert-dialog

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Sixth PR of #520. Migrates the toast surface + confirm dialog + permission dialog onto Base UI Toast + AlertDialog, keeping the useToast() / toast.confirm() API + .maka-toast / .maka-modal CSS + the hand-written confirm queue. Same converge/migration pattern as #526 / #533 / #527 / #539.

Why

Refs #520. Toast, ConfirmDialog, and PermissionDialog were the last three hand-written modals in the renderer — each re-implemented role, focus trap/restore, and Esc handling via useModalA11y, duplicating what Base UI AlertDialog/Toast provide. Converging them onto Base UI removes the duplicate a11y surface and unblocks PR7 (large Base UI migrations).

Scope

Changed:

  • toast.tsxToastProvider/ToastViewport/toast <li> → Base UI Toast (BaseToast.Provider + createToastManager + Viewport/Root/Title/Description/Action/Close, all via render prop to keep <ol>/<li role="alert">/<strong>/<small>/<Button> DOM shape). pushtoastManager.add, dismisstoastManager.close; hand-written toasts state + two-phase dismiss timer deleted (Base UI manages timeout + exit transition). data-exiting maps Base UI transitionStatus="ending" onto the existing .maka-toast[data-exiting="true"] animation. limit={Infinity} (no count cap; existing had none).
  • toast.tsx ConfirmDialogAlertDialogRoot/AlertDialogContent. useModalA11y + dialogRef deleted; cancelRef kept as initialFocus={cancelRef} on the popup. Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. toast.confirm() Promise + queue unchanged.
  • permission-dialog.tsxAlertDialogRoot/AlertDialogContent. Hand-set role="alertdialog" + useModalA11y deleted. Conditional mount preserved (no open prop — defaultOpen + onOpenChange cancel blocks Escape, since a permission request requires an explicit allow/deny). Complex content (reason presets / summary / remember checkbox / danger alert) unchanged.
  • ui.tsx — new AlertDialogRoot/AlertDialogContent wrappers mirroring the Dialog wrappers (Portal + Backdrop + Popup, shared maka-dialog-backdrop hook). Comment corrected (toast.tsx now wraps Base UI Toast, not "left for later").
  • chat-header.css — delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
  • contractstoast-confirm-keyboard-contract (roleAlertDialogRoot, useModalA11yinitialFocus; queue invariants unchanged), ui-tsx-design-contract (z-40/z-50/backdrop-blur-sm counts 1→2), renderer-utility-primitives-contract (import { Button } grep widened to allow AlertDialog imports alongside Button). modal-lifecycle-contract unchanged.

Not included:

  • PR5 (style-hook convention + small Base UI migrations: disclosure→Collapsible, etc.) — in progress in .worktree/base-ui-small-migrations.
  • PR7 (large Base UI migrations: dialog/drawer, combobox, card/table, badge) — waits on PR5 (ui.tsx Badge overlap + style-hook dependency).
  • Toast / confirm / permission visual redesign — .maka-toast / .maka-modal CSS is preserved; only the underlying component layer swaps.

Verification

  • Full desktop suite: 1994/1994 pass (no regressions; rive-workflow flaky under concurrent load, passes in isolation, unrelated).
  • @maka/ui + @maka/desktop build: success.
  • Dual-theme screenshots (turn-narrative + first-run, light + dark): no breakage. Toast / confirm / permission dialogs are state-triggered (not visible in static screenshots); the screenshots confirm the surrounding UI is unchanged.

User-facing impact

No visual change — .maka-toast / .maka-modal CSS and DOM shape are preserved via render props. Behavior parity: confirm/permission focus trap/restore + Esc handling now come from Base UI AlertDialog (equivalent to the prior useModalA11y). Permission dialog Esc stays blocked (onOpenChange cancel). Toast enter/exit animation now driven by Base UI transitionStatus mapped onto the existing data-exiting attribute (same visual).

Reviewer notes

PR6 commit 1: PermissionDialog -> Base UI AlertDialog.
- ui.tsx: add AlertDialog wrappers (AlertDialogRoot/AlertDialogContent) mirroring Dialog wrappers (Portal/Backdrop/Popup, maka-dialog-backdrop hook). AlertDialog locks modal + disables pointer dismissal (Base UI alert-dialog). Escape NOT auto-disabled — callers intercept onOpenChange + details.cancel() to block.
- permission-dialog.tsx: DialogRoot/DialogContent (Base UI Dialog) -> AlertDialogRoot/AlertDialogContent. Delete hand-set role=alertdialog (AlertDialog auto). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep conditional mount (no open prop, parent {activePermission && <PermissionDialog>}) — use defaultOpen + onOpenChange cancel to block Escape (permission requires explicit allow/deny). Keep complex content (reason presets/summary/remember/danger).
- ui-tsx-design-contract: bump z-40/z-50/backdrop-blur-sm counts 1->2 (AlertDialog backdrop/popup shares the hooks).
Full desktop suite 1994/1994 pass. Modal-lifecycle + permission-response-ipc-boundary + renderer-utility-primitives + toast-confirm-keyboard + toast-position-fixed contracts pass.
Tracking issue: #520 PR6.
PR6 commit 2: ConfirmDialog (toast.tsx) -> Base UI AlertDialog.
- toast.tsx: ConfirmDialog hand-written <div role=alertdialog> + useModalA11y -> AlertDialogRoot/AlertDialogContent (Base UI). Delete useModalA11y + dialogRef (Base UI focus trap/restore). Keep cancelRef + initialFocus={cancelRef} (Base UI AlertDialog initialFocus on Popup). Escape/backdrop close = onResolve(false) (cancel) via onOpenChange. Keep toast.confirm Promise + queue (ToastProvider hand-written).
- toast-confirm-keyboard-contract: role=alertdialog grep -> AlertDialogRoot (Base UI auto-sets role); useModalA11y(dialogRef,...,cancelRef) grep -> initialFocus={cancelRef}.
- chat-header.css: delete orphan .maka-confirm-backdrop (confirm now uses shared maka-dialog-backdrop via AlertDialogBackdrop).
- renderer-utility-primitives-contract: import { Button } grep -> import {... Button ...} (allows AlertDialog imports alongside Button).
Full desktop suite 1994/1994 pass.
Tracking issue: #520 PR6.
PR6 commit 3: toast.tsx ToastProvider/ToastViewport/toast li -> Base UI Toast.
- ToastProvider: BaseToast.Provider + BaseToast.createToastManager drive toasts. push -> toastManager.add; dismiss -> toastManager.close. Delete hand-written toasts state + dismiss timer (Base UI manages timeout + exit transition). Keep hand-written confirm queue (activeConfirmRef/confirmQueueRef/resolveConfirm, toast-confirm-keyboard contract) — Base UI Toast has no confirm concept. useToast()/toast/success/error/info/warning/confirm/dismiss API unchanged.
- ToastViewport: <ol> -> BaseToast.Viewport render={<ol .../>}. toast <li> -> BaseToast.Root render={<li role=alert data-variant data-exiting>} + BaseToast.Title/Description/Action/Close (render prop keeps <strong>/<small>/<Button>). data-exiting maps Base UI transitionStatus=ending onto existing .maka-toast[data-exiting=true] exit animation.
- BaseToast.Provider limit={Infinity} (no toast count cap; existing had none).
- ConfirmDialog unchanged (commit 2).
Full desktop suite 1994/1994 pass. toast-position-fixed (TOAST_POSITION=bottom-right + no position prop) + toast-confirm-keyboard (queue) + renderer-utility-primitives (toast buttons) contracts pass.
Tracking issue: #520 PR6.
…PR6)
ui.tsx comment claimed toast.tsx 'already wraps Base UI Toast' — it did not (hand-written). PR6 commit 3 migrated the toast surface to Base UI Toast; update the comment to reflect that. No code change.
…ocus (#520 PR6)
PR6 review P1: continuous confirm queue advance left focus on the confirm button.
ConfirmDialog had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.
Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.
Contract: toast-confirm-keyboard-contract gains a 'remounts on queue advance' case asserting the id generation + key prop.
Real DOM test (activeElement + Enter) deferred to a separate PR — the project has no jsdom/happy-dom DOM test infrastructure today; introducing it is a test-architecture change that doesn't belong in a migration PR.
…mation (#520 PR6)
PR6 review P3.2: drop the redundant data-exiting adapter layer.
Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (confirmed in ToastRootDataAttributes.d.ts). The hand-rolled data-exiting={entry.transitionStatus === 'ending' ...} mapped Base UI's transitionStatus onto a parallel attribute — not the simplest migration.
Drop the JSX adapter; CSS switches .maka-toast[data-exiting="true"] -> .maka-toast[data-ending-style] (1 rule, chat-header.css). Exit animation + pointer-events:none now keyed on Base UI's native attribute.
… shells (#520 PR6)
PR6 review P3.1: AlertDialog wrappers copied Dialog's Backdrop/Popup/Content.
Dialog and AlertDialog differ only in their Base UI primitive family (Root/Portal/Backdrop/Popup/Close); the layout (backdrop class, popup class, Portal+Backdrop+Popup+optional Close structure) is identical. AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (confirmed in alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics needed.
Extract createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent and AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout (z-40/z-50/backdrop-blur-sm/rounded-xl/shadow-maka-panel).
Deletes ~60 lines of duplicate Backdrop/Popup/Content forwardRef wrappers. ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2->1 (no more duplicated className). radius-converge-contract DialogPopup entry -> MODAL_POPUP_CLASS (same rounded-xl modal tier).
Full desktop suite 1995/1995 pass.
@Astro-Han

Copy link
Copy Markdown
ContributorAuthor

Review fixes pushed

Thanks for the review. P1 + P3.1 + P3.2 fixed (3 commits), P2 push-back below.

P1 (blocker) — fixed in 828417b1

Root cause confirmed: {confirmState && <ConfirmDialog request={confirmState} .../>} had no key, so React reused the same instance across queue advances. AlertDialogRoot's uncontrolled defaultOpen + initialFocus only fire on mount — the second confirm inherited the first's focus (stuck on the confirm button), and Enter would mis-confirm a dangerous op.

Fix: PendingConfirm carries a stable id (c{n} from the existing idSeed); <ConfirmDialog key={confirmState.id}> forces a remount on queue advance, so defaultOpen + initialFocus re-run and the cancel button re-takes focus on every new confirm.

Chose key remount over useLayoutEffect focus from first principles: the bug's root is uncontrolled defaultOpen + instance reuse. key remount lets the uncontrolled design work correctly (each confirm = independent AlertDialog lifecycle, single focus mechanism via initialFocus); useLayoutEffect would be a patch layered on the wrong design (two mechanisms managing focus). Only 2 more lines than the effect, and better UX — explicit new-dialog transition instead of same-dialog text swap.

Contract: toast-confirm-keyboard-contract gains a "remounts on queue advance" case asserting the id generation + key prop.

P2 — push back (project infrastructure, not PR6 scope)

The review asks to replace grep contracts with real DOM tests (activeElement + click + Enter). Verified the project has no jsdom/happy-dom — all tests are either source-grep contracts or a hand-rolled fake DOM (FakeElement in ui-render-memo-boundary-contract.test.ts) that has no focus(), document.activeElement, document.body (Portal target), or event system. AlertDialog's Portal + focus trap can't render there.

Introducing happy-dom is a test-architecture change (devDep + lockfile + a new test pattern), which doesn't belong in a Base UI migration PR — it's a separate verification boundary (AGENTS.md PR decomposition). P1's fix is verified by a contract test (key + id); a real DOM test for the continuous-confirm focus path should be a standalone PR that introduces happy-dom and migrates the highest-value grep contracts.

P3.1 — fixed in 20dcc303

AlertDialogBackdrop/Popup/Content were copies of DialogBackdrop/Popup/Content (only BaseDialogBaseAlertDialog differed). Confirmed AlertDialogPopupProps is a direct alias of DialogPopupProps in Base UI (alert-dialog/index.d.ts: DialogPopupProps as AlertDialogPopupProps), so a single concrete type works — no generics.

Extracted createModalContent(primitives) factory: takes {Portal, Backdrop, Popup, Close, defaultShowClose}, returns a forwardRef Content. DialogContent + AlertDialogContent each call it with their Base UI primitives. MODAL_BACKDROP_CLASS + MODAL_POPUP_CLASS constants hold the shared layout. Deletes ~60 lines of duplicate wrappers; ui-tsx-design-contract z-40/z-50/backdrop-blur-sm counts return 2→1; radius-converge-contractDialogPopup entry → MODAL_POPUP_CLASS (same rounded-xl modal tier).

P3.2 — fixed in 919efce3

data-exiting={entry.transitionStatus === 'ending' ...} was a redundant adapter — Base UI ToastRoot already sets data-ending-style (animating out) + data-starting-style (animating in) automatically (ToastRootDataAttributes.d.ts). Dropped the JSX adapter; CSS switches .maka-toast[data-exiting="true"].maka-toast[data-ending-style] (1 rule).

Verification

Full desktop suite 1995/1995 pass (was 1994; +1 from the new "remounts on queue advance" contract case). @maka/ui + @maka/desktop build green.

@Astro-Han
Astro-Han merged commit 01c97c1 into mainJul 5, 2026
@Astro-Han
Astro-Han deleted the refactor/toast-alert-dialog branch July 14, 2026 05:05
Sign up for freeto 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

@Astro-Han