@@ -74,7 +74,7 @@ export interface DialogProps<Payload = unknown> {
7474 * open programmatically as if a given trigger had been activated.
7575 */
7676 triggerId ?: string | null ;
77- /** Content, or a function of `{ payload }` — the `payload` of the active trigger — for per-trigger content . */
77+ /** Content, or a function of `{ payload }` — the active trigger's, or the one `handle.open()` supplied . */
7878 children : ReactNode | ( ( ctx : { payload : Payload | undefined } ) => ReactNode ) ;
7979}
8080
@@ -98,6 +98,10 @@ function DialogInner<Payload>(props: DialogProps<Payload> & { isNested: boolean
9898 // consumed by the floating `onOpenChange` the request funnels into.
9999 const pendingDetailsRef = useRef < DialogOpenChangeDetails | null > ( null ) ;
100100
101+ // The payload of the most recent programmatic `handle.open(payload)`, kept so the registry
102+ // lookup below has something to fall back to when no trigger is involved.
103+ const directPayloadRef = useRef < Payload | undefined > ( undefined ) ;
104+
101105 // Every open/close funnels through `floatingContext.onOpenChange` — trigger activations,
102106 // dismissals, and programmatic `setOpen` alike. floating-ui emits its `openchange` event
103107 // synchronously before invoking this callback, which is what lets listeners (`useReturnFocus`,
@@ -119,6 +123,9 @@ function DialogInner<Payload>(props: DialogProps<Payload> & { isNested: boolean
119123 openFromTrigger : ( id , event ) => {
120124 const registration = store . getTrigger ( id ) ;
121125 setActiveTriggerId ( id ) ;
126+ // A trigger names its own payload, so it supersedes anything a previous programmatic
127+ // open supplied — otherwise the stale one would resurface through the effect below.
128+ directPayloadRef . current = undefined ;
122129 setActivePayload ( registration ?. getPayload ( ) ) ;
123130 if ( registration ) {
124131 refs . setReference ( registration . element ) ;
@@ -131,7 +138,21 @@ function DialogInner<Payload>(props: DialogProps<Payload> & { isNested: boolean
131138 pendingDetailsRef . current = { trigger : registration ?. element ?? null , triggerId : id , event } ;
132139 floatingContext . onOpenChange ( false , event , 'click' ) ;
133140 } ,
134- setOpen : nextOpen => floatingContext . onOpenChange ( nextOpen ) ,
141+ setOpen : ( nextOpen , payload ) => {
142+ // Held in a ref as well as in state because the payload effect below re-runs on `open`
143+ // and would otherwise resolve a trigger-less open to `undefined`, wiping this a commit
144+ // after it was set. Cleared on close as well, so the next payload-less open does not
145+ // resurface this one.
146+ directPayloadRef . current = payload ;
147+ // Published only on the way IN. A close carries no payload, and writing it would blank
148+ // the children-as-function while the popup is still mounted for its exit transition —
149+ // rendering the dialog empty as it leaves, or throwing in a consumer that dereferences
150+ // the payload. The next open sets it afresh.
151+ if ( nextOpen ) {
152+ setActivePayload ( payload ) ;
153+ }
154+ floatingContext . onOpenChange ( nextOpen ) ;
155+ } ,
135156 } ) ;
136157 // `floatingContext` is rebuilt on open/element changes; re-registering is an idempotent swap.
137158 } , [ store , refs , floatingContext , setActiveTriggerId ] ) ;
@@ -155,9 +176,21 @@ function DialogInner<Payload>(props: DialogProps<Payload> & { isNested: boolean
155176 // `defaultOpen` — the payload is looked up from the registry once the dialog is open. Runs
156177 // after the children's layout effects, so triggers rendered inside the root are registered by
157178 // the time it reads, and the pre-paint re-render delivers their payload on the first frame.
179+ //
180+ // An explicit programmatic payload wins over the registry lookup: `activeTriggerId` is never
181+ // reset on close, so once any trigger has opened the dialog the lookup would otherwise overwrite
182+ // every later `handle.open(payload)` with that trigger's payload. The mirror already holds —
183+ // `openFromTrigger` clears `directPayloadRef`, so a trigger wins the other way.
158184 useLayoutEffect ( ( ) => {
159185 if ( open ) {
160- setActivePayload ( activeTriggerId != null ? store . getTrigger ( activeTriggerId ) ?. getPayload ( ) : undefined ) ;
186+ const direct = directPayloadRef . current ;
187+ setActivePayload (
188+ direct !== undefined
189+ ? direct
190+ : activeTriggerId != null
191+ ? store . getTrigger ( activeTriggerId ) ?. getPayload ( )
192+ : undefined ,
193+ ) ;
161194 }
162195 } , [ store , open , activeTriggerId ] ) ;
163196
0 commit comments