Skip to content

Commit 2cc3f5b

Browse files
authored
[transitions] Performance improvements & misc fixes (#48151)
1 parent 27f395b commit 2cc3f5b

9 files changed

Lines changed: 270 additions & 217 deletions

File tree

packages/mui-material/src/Collapse/Collapse.js

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { styled, useTheme } from '../zero-styled';
1010
import memoTheme from '../utils/memoTheme';
1111
import { useDefaultProps } from '../DefaultPropsProvider';
1212
import { duration } from '../styles/createTransitions';
13-
import { getTransitionProps } from '../transitions/utils';
13+
import { normalizedTransitionCallback, getTransitionProps } from '../transitions/utils';
1414
import { useForkRef } from '../utils';
1515
import useSlot from '../utils/useSlot';
1616
import { getCollapseUtilityClass } from './collapseClasses';
@@ -179,23 +179,10 @@ const Collapse = React.forwardRef(function Collapse(inProps, ref) {
179179
const nodeRef = React.useRef(null);
180180
const handleRef = useForkRef(ref, nodeRef);
181181

182-
const normalizedTransitionCallback = (callback) => (maybeIsAppearing) => {
183-
if (callback) {
184-
const node = nodeRef.current;
185-
186-
// onEnterXxx and onExitXxx callbacks have a different arguments.length value.
187-
if (maybeIsAppearing === undefined) {
188-
callback(node);
189-
} else {
190-
callback(node, maybeIsAppearing);
191-
}
192-
}
193-
};
194-
195182
const getWrapperSize = () =>
196183
wrapperRef.current ? wrapperRef.current[isHorizontal ? 'clientWidth' : 'clientHeight'] : 0;
197184

198-
const handleEnter = normalizedTransitionCallback((node, isAppearing) => {
185+
const handleEnter = normalizedTransitionCallback(nodeRef, (node, isAppearing) => {
199186
if (wrapperRef.current && isHorizontal) {
200187
// Set absolute position to get the size of collapsed content
201188
wrapperRef.current.style.position = 'absolute';
@@ -207,7 +194,7 @@ const Collapse = React.forwardRef(function Collapse(inProps, ref) {
207194
}
208195
});
209196

210-
const handleEntering = normalizedTransitionCallback((node, isAppearing) => {
197+
const handleEntering = normalizedTransitionCallback(nodeRef, (node, isAppearing) => {
211198
const wrapperSize = getWrapperSize();
212199

213200
if (wrapperRef.current && isHorizontal) {
@@ -239,25 +226,25 @@ const Collapse = React.forwardRef(function Collapse(inProps, ref) {
239226
}
240227
});
241228

242-
const handleEntered = normalizedTransitionCallback((node, isAppearing) => {
229+
const handleEntered = normalizedTransitionCallback(nodeRef, (node, isAppearing) => {
243230
node.style[size] = 'auto';
244231

245232
if (onEntered) {
246233
onEntered(node, isAppearing);
247234
}
248235
});
249236

250-
const handleExit = normalizedTransitionCallback((node) => {
237+
const handleExit = normalizedTransitionCallback(nodeRef, (node) => {
251238
node.style[size] = `${getWrapperSize()}px`;
252239

253240
if (onExit) {
254241
onExit(node);
255242
}
256243
});
257244

258-
const handleExited = normalizedTransitionCallback(onExited);
245+
const handleExited = normalizedTransitionCallback(nodeRef, onExited);
259246

260-
const handleExiting = normalizedTransitionCallback((node) => {
247+
const handleExiting = normalizedTransitionCallback(nodeRef, (node) => {
261248
const wrapperSize = getWrapperSize();
262249
const { duration: transitionDuration, easing: transitionTimingFunction } = getTransitionProps(
263250
{ style, timeout, easing },

packages/mui-material/src/Fade/Fade.js

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@ import { Transition } from 'react-transition-group';
55
import elementAcceptingRef from '@mui/utils/elementAcceptingRef';
66
import getReactElementRef from '@mui/utils/getReactElementRef';
77
import { useTheme } from '../zero-styled';
8-
import { reflow, getTransitionProps } from '../transitions/utils';
8+
import {
9+
normalizedTransitionCallback,
10+
reflow,
11+
getTransitionProps,
12+
getTransitionChildStyle,
13+
} from '../transitions/utils';
914
import useForkRef from '../utils/useForkRef';
1015

1116
const styles = {
12-
entering: {
13-
opacity: 1,
14-
},
15-
entered: {
16-
opacity: 1,
17-
},
17+
entering: { opacity: 1 },
18+
entered: { opacity: 1 },
19+
exiting: { opacity: 0 },
20+
exited: { opacity: 0 },
1821
};
1922

23+
const hiddenStyles = { opacity: 0, visibility: 'hidden' };
24+
2025
/**
2126
* The Fade transition is used by the [Modal](/material-ui/react-modal/) component.
2227
* It uses [react-transition-group](https://github.com/reactjs/react-transition-group) internally.
@@ -42,31 +47,15 @@ const Fade = React.forwardRef(function Fade(props, ref) {
4247
onExiting,
4348
style,
4449
timeout = defaultTimeout,
45-
// eslint-disable-next-line react/prop-types
46-
TransitionComponent = Transition,
4750
...other
4851
} = props;
4952

50-
const enableStrictModeCompat = true;
5153
const nodeRef = React.useRef(null);
5254
const handleRef = useForkRef(nodeRef, getReactElementRef(children), ref);
5355

54-
const normalizedTransitionCallback = (callback) => (maybeIsAppearing) => {
55-
if (callback) {
56-
const node = nodeRef.current;
57-
58-
// onEnterXxx and onExitXxx callbacks have a different arguments.length value.
59-
if (maybeIsAppearing === undefined) {
60-
callback(node);
61-
} else {
62-
callback(node, maybeIsAppearing);
63-
}
64-
}
65-
};
66-
67-
const handleEntering = normalizedTransitionCallback(onEntering);
56+
const handleEntering = normalizedTransitionCallback(nodeRef, onEntering);
6857

69-
const handleEnter = normalizedTransitionCallback((node, isAppearing) => {
58+
const handleEnter = normalizedTransitionCallback(nodeRef, (node, isAppearing) => {
7059
reflow(node); // So the animation always start from the start.
7160

7261
const transitionProps = getTransitionProps(
@@ -76,35 +65,42 @@ const Fade = React.forwardRef(function Fade(props, ref) {
7665
},
7766
);
7867

79-
node.style.webkitTransition = theme.transitions.create('opacity', transitionProps);
8068
node.style.transition = theme.transitions.create('opacity', transitionProps);
8169

8270
if (onEnter) {
8371
onEnter(node, isAppearing);
8472
}
8573
});
8674

87-
const handleEntered = normalizedTransitionCallback(onEntered);
75+
const handleEntered = normalizedTransitionCallback(nodeRef, onEntered);
8876

89-
const handleExiting = normalizedTransitionCallback(onExiting);
77+
const handleExiting = normalizedTransitionCallback(nodeRef, onExiting);
9078

91-
const handleExit = normalizedTransitionCallback((node) => {
79+
const handleExit = normalizedTransitionCallback(nodeRef, (node) => {
9280
const transitionProps = getTransitionProps(
9381
{ style, timeout, easing },
9482
{
9583
mode: 'exit',
9684
},
9785
);
9886

99-
node.style.webkitTransition = theme.transitions.create('opacity', transitionProps);
10087
node.style.transition = theme.transitions.create('opacity', transitionProps);
10188

10289
if (onExit) {
10390
onExit(node);
10491
}
10592
});
10693

107-
const handleExited = normalizedTransitionCallback(onExited);
94+
const handleExited = normalizedTransitionCallback(nodeRef, (node) => {
95+
// Clear the transition CSS to release the compositor layer when the
96+
// element is fully exited (prevents idle CPU usage on fixed elements
97+
// like Backdrop). handleEnter re-sets it on the next open.
98+
node.style.transition = '';
99+
100+
if (onExited) {
101+
onExited(node);
102+
}
103+
});
108104

109105
const handleAddEndListener = (next) => {
110106
if (addEndListener) {
@@ -114,10 +110,10 @@ const Fade = React.forwardRef(function Fade(props, ref) {
114110
};
115111

116112
return (
117-
<TransitionComponent
113+
<Transition
118114
appear={appear}
119115
in={inProp}
120-
nodeRef={enableStrictModeCompat ? nodeRef : undefined}
116+
nodeRef={nodeRef}
121117
onEnter={handleEnter}
122118
onEntered={handleEntered}
123119
onEntering={handleEntering}
@@ -130,19 +126,22 @@ const Fade = React.forwardRef(function Fade(props, ref) {
130126
>
131127
{/* Ensure "ownerState" is not forwarded to the child DOM element when a direct HTML element is used. This avoids unexpected behavior since "ownerState" is intended for internal styling, component props and not as a DOM attribute. */}
132128
{(state, { ownerState, ...restChildProps }) => {
129+
const childStyle = getTransitionChildStyle(
130+
state,
131+
inProp,
132+
styles,
133+
hiddenStyles,
134+
style,
135+
children.props.style,
136+
);
137+
133138
return React.cloneElement(children, {
134-
style: {
135-
opacity: 0,
136-
visibility: state === 'exited' && !inProp ? 'hidden' : undefined,
137-
...styles[state],
138-
...style,
139-
...children.props.style,
140-
},
139+
style: childStyle,
141140
ref: handleRef,
142141
...restChildProps,
143142
});
144143
}}
145-
</TransitionComponent>
144+
</Transition>
146145
);
147146
});
148147

packages/mui-material/src/Grow/Grow.js

Lines changed: 38 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,26 @@ import elementAcceptingRef from '@mui/utils/elementAcceptingRef';
66
import getReactElementRef from '@mui/utils/getReactElementRef';
77
import { Transition } from 'react-transition-group';
88
import { useTheme } from '../zero-styled';
9-
import { getTransitionProps, reflow } from '../transitions/utils';
9+
import {
10+
normalizedTransitionCallback,
11+
getTransitionProps,
12+
getTransitionChildStyle,
13+
reflow,
14+
} from '../transitions/utils';
1015
import useForkRef from '../utils/useForkRef';
1116

1217
function getScale(value) {
1318
return `scale(${value}, ${value ** 2})`;
1419
}
1520

1621
const styles = {
17-
entering: {
18-
opacity: 1,
19-
transform: getScale(1),
20-
},
21-
entered: {
22-
opacity: 1,
23-
transform: 'none',
24-
},
22+
entering: { opacity: 1, transform: getScale(1) },
23+
entered: { opacity: 1, transform: 'none' },
24+
exiting: { opacity: 0, transform: getScale(0.75) },
25+
exited: { opacity: 0, transform: getScale(0.75) },
2526
};
2627

27-
/*
28-
TODO v6: remove
29-
Conditionally apply a workaround for the CSS transition bug in Safari 15.4 / WebKit browsers.
30-
*/
31-
const isWebKit154 =
32-
typeof navigator !== 'undefined' &&
33-
/^((?!chrome|android).)*(safari|mobile)/i.test(navigator.userAgent) &&
34-
/(os |version\/)15(.|_)4/i.test(navigator.userAgent);
28+
const hiddenStyles = { opacity: 0, transform: getScale(0.75), visibility: 'hidden' };
3529

3630
/**
3731
* The Grow transition is used by the [Tooltip](/material-ui/react-tooltip/) and
@@ -53,8 +47,6 @@ const Grow = React.forwardRef(function Grow(props, ref) {
5347
onExiting,
5448
style,
5549
timeout = 'auto',
56-
// eslint-disable-next-line react/prop-types
57-
TransitionComponent = Transition,
5850
...other
5951
} = props;
6052
const timer = useTimeout();
@@ -64,22 +56,9 @@ const Grow = React.forwardRef(function Grow(props, ref) {
6456
const nodeRef = React.useRef(null);
6557
const handleRef = useForkRef(nodeRef, getReactElementRef(children), ref);
6658

67-
const normalizedTransitionCallback = (callback) => (maybeIsAppearing) => {
68-
if (callback) {
69-
const node = nodeRef.current;
70-
71-
// onEnterXxx and onExitXxx callbacks have a different arguments.length value.
72-
if (maybeIsAppearing === undefined) {
73-
callback(node);
74-
} else {
75-
callback(node, maybeIsAppearing);
76-
}
77-
}
78-
};
79-
80-
const handleEntering = normalizedTransitionCallback(onEntering);
59+
const handleEntering = normalizedTransitionCallback(nodeRef, onEntering);
8160

82-
const handleEnter = normalizedTransitionCallback((node, isAppearing) => {
61+
const handleEnter = normalizedTransitionCallback(nodeRef, (node, isAppearing) => {
8362
reflow(node); // So the animation always start from the start.
8463

8564
const {
@@ -107,7 +86,7 @@ const Grow = React.forwardRef(function Grow(props, ref) {
10786
delay,
10887
}),
10988
theme.transitions.create('transform', {
110-
duration: isWebKit154 ? duration : duration * 0.666,
89+
duration: duration * 0.666,
11190
delay,
11291
easing: transitionTimingFunction,
11392
}),
@@ -118,11 +97,11 @@ const Grow = React.forwardRef(function Grow(props, ref) {
11897
}
11998
});
12099

121-
const handleEntered = normalizedTransitionCallback(onEntered);
100+
const handleEntered = normalizedTransitionCallback(nodeRef, onEntered);
122101

123-
const handleExiting = normalizedTransitionCallback(onExiting);
102+
const handleExiting = normalizedTransitionCallback(nodeRef, onExiting);
124103

125-
const handleExit = normalizedTransitionCallback((node) => {
104+
const handleExit = normalizedTransitionCallback(nodeRef, (node) => {
126105
const {
127106
duration: transitionDuration,
128107
delay,
@@ -148,8 +127,8 @@ const Grow = React.forwardRef(function Grow(props, ref) {
148127
delay,
149128
}),
150129
theme.transitions.create('transform', {
151-
duration: isWebKit154 ? duration : duration * 0.666,
152-
delay: isWebKit154 ? delay : delay || duration * 0.333,
130+
duration: duration * 0.666,
131+
delay: delay || duration * 0.333,
153132
easing: transitionTimingFunction,
154133
}),
155134
].join(',');
@@ -162,7 +141,13 @@ const Grow = React.forwardRef(function Grow(props, ref) {
162141
}
163142
});
164143

165-
const handleExited = normalizedTransitionCallback(onExited);
144+
const handleExited = normalizedTransitionCallback(nodeRef, (node) => {
145+
node.style.transition = '';
146+
147+
if (onExited) {
148+
onExited(node);
149+
}
150+
});
166151

167152
const handleAddEndListener = (next) => {
168153
if (timeout === 'auto') {
@@ -175,7 +160,7 @@ const Grow = React.forwardRef(function Grow(props, ref) {
175160
};
176161

177162
return (
178-
<TransitionComponent
163+
<Transition
179164
appear={appear}
180165
in={inProp}
181166
nodeRef={nodeRef}
@@ -191,20 +176,22 @@ const Grow = React.forwardRef(function Grow(props, ref) {
191176
>
192177
{/* Ensure "ownerState" is not forwarded to the child DOM element when a direct HTML element is used. This avoids unexpected behavior since "ownerState" is intended for internal styling, component props and not as a DOM attribute. */}
193178
{(state, { ownerState, ...restChildProps }) => {
179+
const childStyle = getTransitionChildStyle(
180+
state,
181+
inProp,
182+
styles,
183+
hiddenStyles,
184+
style,
185+
children.props.style,
186+
);
187+
194188
return React.cloneElement(children, {
195-
style: {
196-
opacity: 0,
197-
transform: getScale(0.75),
198-
visibility: state === 'exited' && !inProp ? 'hidden' : undefined,
199-
...styles[state],
200-
...style,
201-
...children.props.style,
202-
},
189+
style: childStyle,
203190
ref: handleRef,
204191
...restChildProps,
205192
});
206193
}}
207-
</TransitionComponent>
194+
</Transition>
208195
);
209196
});
210197

0 commit comments

Comments
 (0)