You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[popover][combobox] Commit request metadata only when the consumer accepts
`setOpen` wrote its whole result before the controlled consumer had a chance to
reflect the request back through `open`. A request the consumer simply ignored —
without cancelling it — therefore left its reason, its transition style and its
unmount override behind in internal state that nothing ever corrected, because
the reconciliation only re-ran when the prop itself changed.
The consequences were live. An ignored hover request left `openReason` on
`trigger-hover`, so a later programmatic open was classified as a hover session:
no initial focus, no trap, no scroll lock and no internal backdrop on a modal
popover. An ignored dismissal left `instantType` on `dismiss`, cancelling the
next transition. In Combobox the same shape reached focus: a declined outside
press latched a close reason that never expired, so the next close skipped the
handoff to the external input and `inert` dropped focus to `<body>`.
Both now follow one rule. A controlled change reflected back in the request's own
synchronous React transaction keeps the interaction's provenance. A prop that
stays put declines the request, and everything it would have committed is
discarded. A prop that moves on its own is programmatic — including a deferred
`startTransition` or an asynchronous store update, which cannot be told apart
from a decline without a public acknowledgement mechanism.
Trigger ownership, raw `open` and `dispatchOpenChange` stay immediate: raw `open`
is what makes a request observable, ownership is overwritten by whichever request
is accepted, and the emitter is load-bearing for close-time focus behaviour.
Reclassifying a session that is already running — an impatient click promoting a
hover-opened popover — commits immediately too, since there is nothing to accept.
`useControlledOpenProvenance` owns the effect ordering that used to be a comment.
Reconciliation has to observe the raw value the request left behind before
`useControlledProp` overwrites the effective one, and a deferred commit has to
land before the effective `open` transition it belongs to is processed —
otherwise a popup the consumer asked to keep mounted would unmount immediately.
Combobox has no raw/effective split to read, and `useControlled`'s setter is
inert while controlled, so a declined request renders nothing on its own. A
request counter forces exactly one pass, and the effect that publishes the
effective `open` value settles the pending reason into `closeReason` in the same
write.
Popover's `openChangeReason` had no readers left and is removed. Menu keeps its
own close/open reason asymmetry, which is deliberately untouched here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0 commit comments