diff --git a/packages/mui-material/package.json b/packages/mui-material/package.json index 3692d62f304084..0716eb5e553135 100644 --- a/packages/mui-material/package.json +++ b/packages/mui-material/package.json @@ -91,6 +91,9 @@ "engines": { "node": ">=14.0.0" }, + "browser": { + "react-transition-group/cjs/TransitionGroupContext.js": "react-transition-group/esm/TransitionGroupContext.js" + }, "exports": { ".": "./src/index.js", "./ButtonBase/TouchRipple": "./src/ButtonBase/TouchRipple.js", diff --git a/packages/mui-material/src/internal/Transition.test.tsx b/packages/mui-material/src/internal/Transition.test.tsx index f792e5fc2c58af..087cef5b8da907 100644 --- a/packages/mui-material/src/internal/Transition.test.tsx +++ b/packages/mui-material/src/internal/Transition.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; +import { TransitionGroup } from 'react-transition-group'; import TransitionGroupContext from 'react-transition-group/TransitionGroupContext'; import { act, createRenderer, screen } from '@mui/internal-test-utils'; import Transition from './Transition'; @@ -990,6 +991,53 @@ describe('', () => { }); }); + describe('react-transition-group public TransitionGroup interop', () => { + it('child added to an already-mounted TransitionGroup enters with isAppearing=false', async () => { + const handlers = { onEnter: spy(), onEntered: spy() }; + let done: (() => void) | null = null; + const addEndListener = (_node: HTMLElement, next: () => void) => { + done = next; + }; + + function ChildWrapper() { + const [shouldRender, setShouldRender] = React.useState(false); + return ( + + + + {shouldRender ? ( + + ) : null} + + + ); + } + + const { user } = render(); + await user.click(screen.getByRole('button', { name: 'add' })); + + expect(screen.getByTestId('target')).to.have.attribute('data-status', 'entering'); + expect(handlers.onEnter.callCount).to.equal(1); + expect(handlers.onEnter.args[0][0]).to.equal(false); + + act(() => { + done!(); + }); + + expect(screen.getByTestId('target')).to.have.attribute('data-status', 'entered'); + expect(handlers.onEntered.callCount).to.equal(1); + expect(handlers.onEntered.args[0][0]).to.equal(false); + }); + }); + describe('react-transition-group TransitionGroupContext user interactions', () => { const mountedGroup = { isMounting: false }; diff --git a/packages/mui-material/src/internal/Transition.tsx b/packages/mui-material/src/internal/Transition.tsx index bde3801241b60e..0b57c0c76f6e80 100644 --- a/packages/mui-material/src/internal/Transition.tsx +++ b/packages/mui-material/src/internal/Transition.tsx @@ -6,7 +6,10 @@ import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useValueAsRef from '@mui/utils/useValueAsRef'; // Material UI transitions must still work inside react-transition-group's TransitionGroup. // Import only its context module; do not import its Transition or TransitionGroup components. -import TransitionGroupContext from 'react-transition-group/TransitionGroupContext'; +// Use RTG's explicit CJS file for Node ESM/SSR; package.json's `browser` field redirects +// browser bundles to RTG's ESM file. +// eslint-disable-next-line import/extensions -- Node ESM needs the explicit .js extension. +import TransitionGroupContext from 'react-transition-group/cjs/TransitionGroupContext.js'; import { reflow } from '../transitions/utils'; type RenderedTransitionStatus = 'entering' | 'entered' | 'exiting' | 'exited'; diff --git a/packages/mui-material/src/internal/react-transition-group.d.ts b/packages/mui-material/src/internal/react-transition-group.d.ts index ef20f1c36ce0cd..bca21db721ead7 100644 --- a/packages/mui-material/src/internal/react-transition-group.d.ts +++ b/packages/mui-material/src/internal/react-transition-group.d.ts @@ -8,3 +8,14 @@ declare module 'react-transition-group/TransitionGroupContext' { const TransitionGroupContext: React.Context; export default TransitionGroupContext; } + +declare module 'react-transition-group/cjs/TransitionGroupContext.js' { + import * as React from 'react'; + + interface TransitionGroupContextValue { + isMounting: boolean; + } + + const TransitionGroupContext: React.Context; + export default TransitionGroupContext; +}