@mui/material
Version:
Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.
450 lines (436 loc) • 15.4 kB
JavaScript
"use strict";
/// <reference path="./react-transition-group.d.ts" />
'use client';
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect"));
var _useValueAsRef = _interopRequireDefault(require("@mui/utils/useValueAsRef"));
var _TransitionGroupContext = _interopRequireDefault(require("react-transition-group/TransitionGroupContext"));
var _utils = require("../transitions/utils");
var _jsxRuntime = require("react/jsx-runtime");
// 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.
function resolveTimeouts(timeout) {
if (timeout == null) {
return {
appear: undefined,
enter: undefined,
exit: undefined
};
}
if (typeof timeout === 'number') {
return {
appear: timeout,
enter: timeout,
exit: timeout
};
}
const enter = timeout.enter;
const exit = timeout.exit;
const appear = timeout.appear !== undefined ? timeout.appear : enter;
return {
appear,
enter,
exit
};
}
/**
* Resolves the authored completion timeout for the current transition phase.
* Auto durations are read by the caller at scheduling time so Grow/Collapse
* can pass the latest measured value without storing it in React state.
*/
function getCompletionTimeout(params) {
if (params.autoTimeout != null) {
return params.autoTimeout;
}
const resolved = resolveTimeouts(params.timeout);
if (params.currentStatus === 'entering') {
return params.isAppearing ? resolved.appear ?? resolved.enter ?? null : resolved.enter ?? null;
}
return resolved.exit ?? null;
}
function Transition(props) {
const {
in: inProp = false,
appear = false,
enter = true,
exit = true,
mountOnEnter = false,
unmountOnExit = false,
timeout,
addEndListener,
reduceMotion = false,
getAutoTimeout,
nodeRef,
onEnter,
onEntering,
onEntered,
onExit,
onExiting,
onExited,
children,
...childProps
} = props;
const parentGroup = React.useContext(_TransitionGroupContext.default);
// react-transition-group's TransitionGroup tells children whether the group
// is still mounting. Material UI needs two values from that:
// - shouldEnterOnMount: whether this child should run an enter animation now.
// - isAppearing: the value passed to enter callbacks.
// A child added after the group mounted still enters, but callbacks receive
// isAppearing=false because the parent group is no longer mounting.
const shouldEnterOnMount = parentGroup && !parentGroup.isMounting ? enter : appear;
const [status, setStatus] = React.useState(() => {
if (inProp) {
return shouldEnterOnMount ? 'exited' : 'entered';
}
if (mountOnEnter || unmountOnExit) {
return 'unmounted';
}
return 'exited';
});
const statusRef = React.useRef(status);
statusRef.current = status;
// Opening from `unmounted`: mount the child in the same commit that `in` turns
// true so its ref is attached before effects run. react-transition-group did
// this by deriving the status from props during render; handling it in a
// layout effect instead would add a commit where the child is still null,
// breaking consumers that read the ref right after `in` flips.
if (inProp && status === 'unmounted') {
statusRef.current = 'exited';
setStatus('exited');
}
const shouldAppearOnMountRef = React.useRef(inProp && shouldEnterOnMount);
const mountedRef = React.useRef(false);
const nextCallbackRef = React.useRef(null);
// Remember which status already fired lifecycle callbacks. React StrictMode
// can run effects twice in development; this prevents duplicate callbacks.
const lastFiredStatusRef = React.useRef(status);
// Store the isAppearing value for the current enter transition. performEnter
// sets it before the status effect later calls onEntering/onEntered.
const isAppearingRef = React.useRef(false);
// Capture reduced motion at the start of each phase so prop updates do not
// change the completion timing for an active transition.
const transitionReduceMotionRef = React.useRef(reduceMotion);
// Transition end callbacks can run after props changed. Read props through
// this ref so delayed work uses the latest callbacks and timeout settings.
const propsRef = (0, _useValueAsRef.default)({
timeout,
addEndListener,
reduceMotion,
getAutoTimeout,
onEnter,
onEntering,
onEntered,
onExit,
onExiting,
onExited,
enter,
exit,
mountOnEnter,
unmountOnExit,
nodeRef,
parentGroup
});
// Effects below depend on these helpers. Keep their identity stable; they read
// changing props through propsRef.
const cancelPendingCallback = React.useCallback(() => {
if (nextCallbackRef.current !== null) {
nextCallbackRef.current.cancel();
nextCallbackRef.current = null;
}
}, []);
const makeCallback = React.useCallback(handler => {
let active = true;
const wrapped = () => {
if (active) {
active = false;
nextCallbackRef.current = null;
handler();
}
};
wrapped.cancel = () => {
active = false;
};
nextCallbackRef.current = wrapped;
return wrapped;
}, []);
const scheduleTransitionEnd = React.useCallback((nextStatus, currentStatus) => {
let timeoutId;
const clearTimer = () => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
};
const done = makeCallback(() => {
clearTimer();
statusRef.current = nextStatus;
setStatus(nextStatus);
});
const cancelDone = done.cancel;
done.cancel = () => {
clearTimer();
cancelDone();
};
const node = propsRef.current.nodeRef.current;
const listener = propsRef.current.addEndListener;
const hasAutoTimeout = propsRef.current.getAutoTimeout !== undefined;
const autoTimeout = propsRef.current.getAutoTimeout?.();
const authoredTimeout = getCompletionTimeout({
currentStatus,
isAppearing: isAppearingRef.current,
timeout: propsRef.current.timeout,
autoTimeout
});
const transitionReduceMotion = transitionReduceMotionRef.current;
// Auto-duration consumers may skip measurement under reduced motion, but
// still need a 0ms timeout when they provide addEndListener.
const fallbackTimeout = authoredTimeout ?? (transitionReduceMotion && hasAutoTimeout ? 0 : null);
const scheduleTimer = value => {
timeoutId = setTimeout(done, value);
};
if (!node) {
if (process.env.NODE_ENV !== 'production') {
console.warn(['MUI: The transition child does not expose a DOM element.', 'Make sure the child accepts a ref and forwards it to the underlying DOM element.', 'The transition animation cannot be observed without a DOM element and will be skipped.'].join('\n'));
}
// Match react-transition-group: if there is no DOM node, there is no
// transition to observe, so finish on the next tick.
scheduleTimer(0);
return;
}
if (listener) {
if (fallbackTimeout != null) {
scheduleTimer(transitionReduceMotion ? 0 : fallbackTimeout);
}
// With nodeRef, react-transition-group calls addEndListener(done).
// Material UI has long supported addEndListener(node, done). Keep both call
// shapes so existing transition wrappers do not have to change.
if (listener.length >= 2) {
listener(node, done);
} else {
listener(done);
}
return;
}
scheduleTimer(transitionReduceMotion ? 0 : authoredTimeout ?? 0);
}, [makeCallback, propsRef]);
const performEnter = React.useCallback(mounting => {
const current = propsRef.current;
const isAppearing = current.parentGroup ? current.parentGroup.isMounting : mounting;
isAppearingRef.current = isAppearing;
// On updates, enter=false skips the enter animation. Move straight to
// entered; the status effect will call onEntered, but onEnter/onEntering
// must not fire.
if (!mounting && !current.enter) {
statusRef.current = 'entered';
setStatus('entered');
return;
}
transitionReduceMotionRef.current = current.reduceMotion;
current.onEnter?.(isAppearing);
statusRef.current = 'entering';
setStatus('entering');
}, [propsRef]);
const performExit = React.useCallback(() => {
const current = propsRef.current;
if (!current.exit) {
statusRef.current = 'exited';
setStatus('exited');
return;
}
transitionReduceMotionRef.current = current.reduceMotion;
current.onExit?.();
statusRef.current = 'exiting';
setStatus('exiting');
}, [propsRef]);
const updateStatus = React.useCallback((mounting, nextStatus) => {
cancelPendingCallback();
if (nextStatus === 'entering') {
const current = propsRef.current;
// If the node was just mounted, read layout before entering so the
// browser applies the starting styles before the animation begins.
if (current.mountOnEnter || current.unmountOnExit) {
const node = current.nodeRef.current;
if (node) {
(0, _utils.reflow)(node);
}
}
performEnter(mounting);
} else {
performExit();
}
}, [cancelPendingCallback, performEnter, performExit, propsRef]);
// Runs on mount. useEnhancedEffect is needed because the initial appear
// transition may read layout before paint. In StrictMode development builds,
// React mounts, cleans up, and mounts again; cleanup cancels pending work and
// the second mount restarts the same transition.
(0, _useEnhancedEffect.default)(() => {
mountedRef.current = true;
if (shouldAppearOnMountRef.current) {
shouldAppearOnMountRef.current = false;
updateStatus(true, 'entering');
}
return () => {
mountedRef.current = false;
cancelPendingCallback();
};
}, [cancelPendingCallback, updateStatus]);
// Reconcile the rendered status after `in` or status changes:
// - opening from unmounted is handled during render (see above) so the child
// is committed as exited with its ref attached before this effect runs.
// - unmountOnExit removes the child after the exited state commits.
// This matches react-transition-group's observable status steps without
// running work after unrelated commits.
(0, _useEnhancedEffect.default)(() => {
if (!mountedRef.current) {
return;
}
const current = statusRef.current;
if (inProp) {
if (current !== 'entering' && current !== 'entered') {
updateStatus(false, 'entering');
}
} else if (current === 'entering' || current === 'entered') {
updateStatus(false, 'exiting');
} else if (current === 'exited' && unmountOnExit) {
statusRef.current = 'unmounted';
setStatus('unmounted');
}
}, [inProp, status, unmountOnExit, updateStatus]);
// Fire lifecycle callbacks for committed status changes. The guard prevents
// duplicate callbacks in StrictMode; propsRef keeps delayed callbacks fresh.
(0, _useEnhancedEffect.default)(() => {
// `unmounted` is bookkeeping, not a real transition state. Do not fire
// callbacks when moving into or out of it; otherwise the first open with
// mountOnEnter/unmountOnExit would look like a completed exit.
if (status === 'unmounted' || lastFiredStatusRef.current === 'unmounted') {
lastFiredStatusRef.current = status;
return;
}
const prev = lastFiredStatusRef.current;
if (prev === status) {
return;
}
lastFiredStatusRef.current = status;
const current = propsRef.current;
if (status === 'entering') {
current.onEntering?.(isAppearingRef.current);
scheduleTransitionEnd('entered', 'entering');
} else if (status === 'exiting') {
current.onExiting?.();
scheduleTransitionEnd('exited', 'exiting');
} else if (status === 'entered') {
current.onEntered?.(isAppearingRef.current);
} else if (status === 'exited') {
current.onExited?.();
}
}, [propsRef, scheduleTransitionEnd, status]);
if (status === 'unmounted') {
return null;
}
// Nested Material UI transitions should not inherit this transition's parent group.
// A null context keeps an outer TransitionGroup from controlling them.
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_TransitionGroupContext.default.Provider, {
value: null,
children: children(status, childProps)
});
}
process.env.NODE_ENV !== "production" ? Transition.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* @ignore
*/
addEndListener: _propTypes.default.func,
/**
* @ignore
*/
appear: _propTypes.default.bool,
/**
* @ignore
*/
children: _propTypes.default.func.isRequired,
/**
* @ignore
*/
enter: _propTypes.default.bool,
/**
* @ignore
*/
exit: _propTypes.default.bool,
/**
* @ignore
*/
getAutoTimeout: _propTypes.default.func,
/**
* @ignore
*/
in: _propTypes.default.bool,
/**
* @ignore
*/
mountOnEnter: _propTypes.default.bool,
/**
* @ignore
*/
nodeRef: _propTypes.default.shape({
current: (props, propName) => {
if (props[propName] == null) {
return null;
}
if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {
return new Error(`Expected prop '${propName}' to be of type Element`);
}
return null;
}
}).isRequired,
/**
* @ignore
*/
onEnter: _propTypes.default.func,
/**
* @ignore
*/
onEntered: _propTypes.default.func,
/**
* @ignore
*/
onEntering: _propTypes.default.func,
/**
* @ignore
*/
onExit: _propTypes.default.func,
/**
* @ignore
*/
onExited: _propTypes.default.func,
/**
* @ignore
*/
onExiting: _propTypes.default.func,
/**
* @ignore
*/
reduceMotion: _propTypes.default.bool,
/**
* @ignore
*/
timeout: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.shape({
appear: _propTypes.default.number,
enter: _propTypes.default.number,
exit: _propTypes.default.number
})]),
/**
* @ignore
*/
unmountOnExit: _propTypes.default.bool
} : void 0;
var _default = exports.default = Transition;