[alert dialog] Fix handle defaults#4834
Conversation
commit: |
Bundle size
PerformanceTotal duration: 1,162.73 ms -9.02 ms(-0.8%) | Renders: 50 (+0) | Paint: 1,781.85 ms +2.75 ms(+0.2%)
11 tests within noise — details Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
2b7889f to
5b2f20e
Compare
flaviendelangle
left a comment
There was a problem hiding this comment.
PR #4834 Review Summary — [alert dialog] Fix handle defaults
Critical Issues
-
Invalid test assertion (
pr-test-analyzer,code-reviewer) — at AlertDialogRoot.test.tsx:205,expect(viewport).toContain(popup)is the Vitest core matcher (strings/arrays/iterables), not the jest-dom DOM-containment matcher (toContainElement). The code-reviewer reports it passes in jsdom because Vitest'stoContainhas a DOM-node branch that callsactual.contains(item)— so this is an important discrepancy in the two analyses; verification recommended (run the test and inspect Vitest's matcher source). IftoContainElementis the correct API per jest-dom, swap it. -
AlertDialogHandleis structurally identical toDialogHandle— type narrowing is illusory (type-design-analyzer,silent-failure-hunter) — the new class adds no instance members, so TS structural typing accepts a plainDialogHandleanywhereAlertDialogHandleis expected. The "fix" of narrowingAlertDialogRootProps.handleandAlertDialogTriggerProps.handletoAlertDialogHandleprovides no real safety. Fix: add a private brand (declare private readonly __alertDialog: unique symbol;) onAlertDialogHandle.
Important Issues
-
No direct regression test for the original finding #1 (
pr-test-analyzer) — the PR claims to fix "publicAlertDialog.Handlecan silently create a non-alert dialog", but every existing test usesAlertDialog.createHandle()(which already builds an alert-defaulted store). Removing the newthis.store.update(alertDialogState)line at handle.ts:16 would not fail any current test. Add a test that constructsnew AlertDialog.Handle(new DialogStore())with a plain store and assertsrole='alertdialog', modal, no pointer dismissal. -
No test file for
AlertDialogTrigger(pr-test-analyzer) — per AGENTS.md convention, components get co-located.test.tsxfiles. -
Silent overwrite of caller-supplied store in
AlertDialogHandleconstructor (silent-failure-hunter) — handle.ts:14-17 unconditionally callsthis.store.update(alertDialogState), silently mutating a foreign store'smodal/disablePointerDismissal/role. Add a dev-mode warning when the supplied store has conflicting state. -
useRenderDialogRootwrites to store during render (silent-failure-hunter) — useRenderDialogRoot.tsx:44-56 the alert branch always callsstore.update(rootState)synchronously during render (viauseOnFirstRender), which can interact badly with concurrent/strict mode. Consider moving touseIsoLayoutEffect. -
Race in
forceUnmountresettingpreventUnmountingOnClose(silent-failure-hunter) — popupStoreUtils.ts:264-274 unconditionally resets the flag; a previously-armed animation-completion listener firing after a user re-opens then re-closes withpreventUnmountOnClose()could force-unmount despite user intent. Gate theonCompletecallback with a freshstore.state.preventUnmountingOnClosecheck, or attach the reset to an explicit "manual unmount" code path.
Suggestions
-
Positional boolean args (
code-reviewer,type-design-analyzer,comment-analyzer) —useRenderDialogRoot(props, false, true)is opaque; prefermode: 'dialog' | 'drawer' | 'alertdialog'or an options object. Illegal combos (isDrawer && isAlertDialog) are currently representable. -
alertDialogStateduplicated (type-design-analyzer) — the same defaults are encoded once in handle.ts:4-8 and again as ternaries in useRenderDialogRoot.tsx:28-34. Consolidate. -
Missing comments on load-bearing branches (
comment-analyzer,code-reviewer) — the alert-onlystore.update(rootState)inuseOnFirstRenderand the duplicatethis.store.update(alertDialogState)in theAlertDialogHandleconstructor are both invisible-purpose code paths that future cleanups will likely remove. Add one-line comments explaining the invariant. -
Inconsistent terminology cleanup in
AlertDialogRootJSDoc (comment-analyzer) —actionsRefandonOpenChangeJSDoc at AlertDialogRoot.tsx:24-36 still says "the dialog" while the PR migrated trigger/handle docs to "the alert dialog". -
Pre-existing trigger issues now natural to fix (
code-reviewer) —aria-haspopupis hard-coded to'dialog'inDialogTrigger, and error messages referenceDialog.Triggereven for alert callers. The dedicatedAlertDialogTriggermakes this fixable, but the type-cast trick (DialogTrigger as AlertDialogTrigger) leaves no place to specialize. -
Redundant
updatecall when no store is passed (code-reviewer) — inAlertDialogHandleconstructor, whenstoreis undefined the freshly-builtDialogStorealready has alert defaults; the subsequentupdateis a no-op.
Strengths
- The three documented bugs are fixed correctly; the shared
useRenderDialogRootrefactor matches AGENTS.md's "avoid duplicating logic" guideline. - The
preventUnmountingOnClose: falsereset in popupStoreUtils.ts automatically fixes the same bug across Dialog, Drawer, Menu, Popover, PreviewCard, and Tooltip. - Tests adopt
React.createRef<AlertDialog.Root.Actions>()instead of ad-hoc mocks, exercising real public types. - The "clears manual unmount state" test cleanly reproduces and verifies finding #3.
- AGENTS.md compliance (useTimeout/useStableCallback/useIsoLayoutEffect, shadow-DOM-safe utils, ownerDocument/Window) is clean.
- The code-reviewer ran
pnpm typescript,pnpm eslint, and the relevant jsdom test suites — all pass.
Recommended Action
- Verify the
toContainassertion by runningpnpm test:jsdom AlertDialog --no-watchand inspecting whether it actually fails when the assertion is broken (e.g., by temporarily changing the viewport to a sibling). If it doesn't, replace withtoContainElement. - Add the brand to
AlertDialogHandle— single line, makes the entire type-narrowing story real. - Add the missing regression test for
new AlertDialog.Handle(plainStore)(covers original finding #1). - Add explanatory comments on the two load-bearing branches (handle.ts:16, useRenderDialogRoot.tsx alert branch).
- Consider the positional-boolean → discriminated-mode refactor and the actionsRef/onOpenChange JSDoc terminology cleanup before merging.
- Defer the race-condition fix in
forceUnmountfor a follow-up unless reproducible.
5b2f20e to
2a07d83
Compare
|
@flaviendelangle Claude overstating a bit Codex:
|
36fa9d2 to
3322d2b
Compare
This is part of the Codex component behavior and test coverage sweep. It fixes Alert Dialog behavior/API issues found during review and adds focused regression coverage for the affected paths.
Root cause
Alert Dialog reused Dialog store and handle paths that did not always enforce alert-dialog defaults, and the shared manual-unmount flag was not reset after actionsRef.unmount().
Changes
Original findings addressed
AlertDialog.Handlecan silently create a non-alert dialog.defaultOpenis ignored whenAlertDialog.Rootuses a handle.preventUnmountOnClose()state survives manual unmount and breaks later closes.