-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
Bug Report
Package version
@fluentui/react-components@9.73.1 (works correctly in 9.72.10)
Description
A Dialog becomes impossible to close (both via DialogTrigger close button and programmatic setIsOpen(false)) after a MessageBar rendered inside its content unmounts.
Reproduction
Stackblitz: https://stackblitz.com/github/layershifter/fluentui-motion-ref-collision-repro
Repo: https://github.com/layershifter/fluentui-motion-ref-collision-repro
Inline code
import { useState, useEffect } from "react";
import {
FluentProvider,
webLightTheme,
Dialog,
DialogTrigger,
DialogSurface,
DialogBody,
DialogTitle,
DialogContent,
DialogActions,
Button,
MessageBar,
MessageBarBody,
} from "@fluentui/react-components";
function App() {
const [isOpen, setIsOpen] = useState(false);
const [showMessageBar, setShowMessageBar] = useState(true);
useEffect(() => {
if (isOpen) {
setShowMessageBar(true);
const timer = setTimeout(() => setShowMessageBar(false), 2000);
return () => clearTimeout(timer);
}
}, [isOpen]);
return (
<FluentProvider theme={webLightTheme}>
<Dialog
open={isOpen}
onOpenChange={(_event, data) => setIsOpen(data.open)}
>
<DialogTrigger disableButtonEnhancement>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>Bug Repro</DialogTitle>
<DialogContent>
{showMessageBar && (
<MessageBar>
<MessageBarBody>
This message disappears in 2s — then the dialog breaks.
</MessageBarBody>
</MessageBar>
)}
<p>After the MessageBar disappears, try closing this dialog.</p>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary">Close</Button>
</DialogTrigger>
<Button appearance="primary" onClick={() => setIsOpen(false)}>
Action & Close
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</FluentProvider>
);
}Steps to reproduce
- Open the Dialog
- Wait 2 seconds for the MessageBar to unmount
- Click "Close" or "Action & Close"
Expected behavior
The Dialog closes normally.
Actual behavior
The Dialog does not close. onOpenChange fires, setIsOpen(false) is called, but the surfaceMotion presence component cannot run the exit animation because childRef.current was set to null when the MessageBar unmounted.
Root Cause
Introduced by #35774 ("feat(react-motion): add MotionRefForwarder helper").
That PR moved MotionRefForwarder and useMotionForwardedRef from private per-package modules to a shared export in @fluentui/react-motion. This means react-dialog and react-message-bar now share the same React.createContext() instance (MotionRefForwarderContext).
Collision mechanism:
renderDialogwraps content with<MotionRefForwarder>, providing thesurfaceMotion's ref to the shared context- A standalone
<MessageBar>inside the Dialog callsuseMotionForwardedRef()inuseMessageBar— reads the Dialog's motion ref (nearest provider on the now-shared context) useMergedRefs(ref, ..., motionRef)inuseMessageBarassigns the MessageBar's DOM element to the Dialog'schildRef.current, overwriting the Dialog surface element reference- When the MessageBar unmounts →
childRef.currentis set tonull - The Dialog's
surfaceMotionpresence component can no longer find the element to run the exit animation → Dialog stays open
Previous behavior (9.72.10): Each package had its own private MotionRefForwarderContext (react-dialog/lib/components/MotionRefForwarder.js and react-message-bar/lib/components/MotionRefForwarder.js each called React.createContext() independently). A MessageBar inside a Dialog would call useMotionForwardedRef() on its own private context → no provider → returned undefined → harmlessly ignored by useMergedRefs.
Affected packages
@fluentui/react-dialog(9.16.6 → 9.17.1)@fluentui/react-message-bar(9.6.17 → 9.6.19)@fluentui/react-motion(9.11.6 → 9.12.0)