react-transition-preset
Version:
Lightweight, zero-dependency transition component for React with common preset transition
538 lines (526 loc) • 15.6 kB
JavaScript
;
var react = require('react');
var jsxRuntime = require('react/jsx-runtime');
// src/Transition.tsx
// src/presets.ts
var pop = {
center: 0,
top: -16,
bottom: 16
};
var popIn = (from) => ({
in: { opacity: 1, transform: "scale(1)" },
out: {
opacity: 0,
transform: `scale(var(--transition-preset-pop-in-${from}-scale, 0.9)) translateY(var(--transition-preset-pop-in-${from}, ${pop[from]}px))`
},
transitionProperty: "transform, opacity"
});
var presets = {
"fade": {
in: { opacity: 1 },
out: { opacity: 0 },
transitionProperty: "opacity"
},
"fade-up": {
in: { opacity: 1, transform: "translateY(0)" },
out: { opacity: 0, transform: `translateY(var(--transition-preset-fade-up, -16px))` },
transitionProperty: "opacity, transform"
},
"fade-down": {
in: { opacity: 1, transform: "translateY(0)" },
out: { opacity: 0, transform: `translateY(var(--transtion-preset-fade-down, 16px)` },
transitionProperty: "opacity, transform"
},
"fade-left": {
in: { opacity: 1, transform: "translateX(0)" },
out: { opacity: 0, transform: `translateX(var(--transition-preset-fade-left, -16px))` },
transitionProperty: "opacity, transform"
},
"fade-right": {
in: { opacity: 1, transform: "translateX(0)" },
out: { opacity: 0, transform: `translateX(var(--transition-preset-fade-right, 16px)` },
transitionProperty: "opacity, transform"
},
"scale": {
in: { opacity: 1, transform: "scale(1)" },
out: { opacity: 0, transform: "scale(0)" },
common: { transformOrigin: "top" },
transitionProperty: "transform, opacity"
},
"scale-y": {
in: { opacity: 1, transform: "scaleY(1)" },
out: { opacity: 0, transform: "scaleY(0)" },
common: { transformOrigin: "top" },
transitionProperty: "transform, opacity"
},
"scale-x": {
in: { opacity: 1, transform: "scaleX(1)" },
out: { opacity: 0, transform: "scaleX(0)" },
common: { transformOrigin: "left" },
transitionProperty: "transform, opacity"
},
"skew-up": {
in: { opacity: 1, transform: "translateY(0) skew(0deg, 0deg)" },
out: {
opacity: 0,
transform: `translateY(var(--transition-preset-skew-up, -20px)) skew(var(--transition-preset-skew-up-deg, -10deg, -5deg))`
},
common: { transformOrigin: "top" },
transitionProperty: "transform, opacity"
},
"skew-down": {
in: { opacity: 1, transform: "translateY(0) skew(0deg, 0deg)" },
out: {
opacity: 0,
transform: `translateY(var(--transition-preset-skew-down, 20px)) skew(var(--transition-preset-skew-down-deg, -10deg, -5deg))`
},
common: { transformOrigin: "bottom" },
transitionProperty: "transform, opacity"
},
"rotate-left": {
in: { opacity: 1, transform: "translateY(0) rotate(0deg)" },
out: {
opacity: 0,
transform: `translateY(var(--transition-preset-rotate-left, 20px)) rotate(var(--transition-preset-rotate-left-deg, -5deg))`
},
common: { transformOrigin: "bottom" },
transitionProperty: "transform, opacity"
},
"rotate-right": {
in: { opacity: 1, transform: "translateY(0) rotate(0deg)" },
out: {
opacity: 0,
transform: `translateY(var(--transition-preset-rotate-right, 20px)) rotate(var(--transition-preset-rotate-right-deg, 5deg))`
},
common: { transformOrigin: "top" },
transitionProperty: "transform, opacity"
},
"slide-down": {
in: { opacity: 1, transform: "translateY(0)" },
out: { opacity: 0, transform: "translateY(100%)" },
common: { transformOrigin: "top" },
transitionProperty: "transform, opacity"
},
"slide-up": {
in: { opacity: 1, transform: "translateY(0)" },
out: { opacity: 0, transform: "translateY(-100%)" },
common: { transformOrigin: "bottom" },
transitionProperty: "transform, opacity"
},
"slide-left": {
in: { opacity: 1, transform: "translateX(0)" },
out: { opacity: 0, transform: "translateX(-100%)" },
common: { transformOrigin: "left" },
transitionProperty: "transform, opacity"
},
"slide-right": {
in: { opacity: 1, transform: "translateX(0)" },
out: { opacity: 0, transform: "translateX(100%)" },
common: { transformOrigin: "right" },
transitionProperty: "transform, opacity"
},
"pop": {
...popIn("center"),
common: { transformOrigin: "center center" }
},
"pop-top": {
...popIn("top"),
common: { transformOrigin: "top center" }
},
"pop-bottom": {
...popIn("bottom"),
common: { transformOrigin: "bottom center" }
},
"pop-left": {
...popIn("center"),
common: { transformOrigin: "center left" }
},
"pop-right": {
...popIn("center"),
common: { transformOrigin: "center right" }
},
"pop-bottom-left": {
...popIn("bottom"),
common: { transformOrigin: "bottom left" }
},
"pop-bottom-right": {
...popIn("bottom"),
common: { transformOrigin: "bottom right" }
},
"pop-top-left": {
...popIn("top"),
common: { transformOrigin: "top left" }
},
"pop-top-right": {
...popIn("top"),
common: { transformOrigin: "top right" }
}
};
var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
var useIsomorphicLayoutEffect = isBrowser ? react.useLayoutEffect : react.useEffect;
// src/hooks/use-did-update.ts
function useDidUpdate(fn, options) {
const { initialMounted, deps } = options;
const mounted = react.useRef(initialMounted);
useIsomorphicLayoutEffect(
() => () => {
mounted.current = initialMounted;
},
[]
);
useIsomorphicLayoutEffect(() => {
if (mounted.current) {
fn();
return;
}
mounted.current = true;
return void 0;
}, deps);
}
function useMemoizedFn(fn) {
const fnRef = react.useRef(fn);
fnRef.current = react.useMemo(() => fn, [fn]);
const memoizedFn = react.useRef();
if (!memoizedFn.current) {
memoizedFn.current = function(...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current;
}
// src/utils.ts
function defaults(options, defaultOptions) {
const result = { ...defaultOptions };
for (const key in options) {
if (options[key] !== void 0) {
result[key] = options[key];
}
}
return result;
}
function secToMs(ms) {
return (ms || 0) * 1e3;
}
// src/use-transition.ts
function useTransition(props) {
const {
duration: durationInSec,
initial,
exitDuration: exitDurationInSec,
timingFunction,
mounted,
reduceMotion,
onEnter,
onExit,
onEntered,
onExited,
enterDelay: enterDelayInSec,
exitDelay: exitDelayInSec
} = props;
const [duration, exitDuration, enterDelay, exitDelay] = [
durationInSec,
exitDurationInSec,
enterDelayInSec,
exitDelayInSec
].map(secToMs);
const [transitionDuration, setTransitionDuration] = react.useState(reduceMotion ? 0 : duration);
const [transitionStatus, setStatus] = react.useState(() => {
if (mounted) {
return initial ? "pre-entering" /* preEntering */ : "entered" /* entered */;
}
return "exited" /* exited */;
});
const transitionTimeoutRef = react.useRef(-1);
const delayTimeoutRef = react.useRef(-1);
const rafRef = react.useRef(-1);
const handleStateChange = useMemoizedFn((shouldMount) => {
const preHandler = shouldMount ? onEnter : onExit;
const handler = shouldMount ? onEntered : onExited;
window.clearTimeout(transitionTimeoutRef.current);
const newTransitionDuration = reduceMotion ? 0 : shouldMount ? duration : exitDuration;
setTransitionDuration(newTransitionDuration);
if (newTransitionDuration === 0) {
typeof preHandler === "function" && preHandler();
typeof handler === "function" && handler();
setStatus(shouldMount ? "entered" /* entered */ : "exited" /* exited */);
} else {
rafRef.current = requestAnimationFrame(() => {
setStatus(shouldMount ? "pre-entering" /* preEntering */ : "pre-exiting" /* preExiting */);
rafRef.current = requestAnimationFrame(() => {
typeof preHandler === "function" && preHandler();
setStatus(shouldMount ? "entering" /* entering */ : "exiting" /* exiting */);
transitionTimeoutRef.current = window.setTimeout(() => {
typeof handler === "function" && handler();
setStatus(shouldMount ? "entered" /* entered */ : "exited" /* exited */);
}, newTransitionDuration);
});
});
}
});
const handleTransitionWithDelay = useMemoizedFn((shouldMount) => {
window.clearTimeout(delayTimeoutRef.current);
const delay = shouldMount ? enterDelay : exitDelay;
if (typeof delay !== "number") {
handleStateChange(shouldMount);
return;
}
delayTimeoutRef.current = window.setTimeout(
() => {
handleStateChange(shouldMount);
},
shouldMount ? enterDelay : exitDelay
);
});
useDidUpdate(
() => {
handleTransitionWithDelay(mounted);
},
{
deps: [mounted],
initialMounted: initial && mounted
}
);
useIsomorphicLayoutEffect(
() => () => {
window.clearTimeout(transitionTimeoutRef.current);
cancelAnimationFrame(rafRef.current);
},
[]
);
return {
transitionDuration,
transitionStatus,
transitionTimingFunction: timingFunction || "ease"
};
}
// src/get-transition-styles/get-transition-styles.ts
var transitionStatuses = {
["entering" /* entering */]: "in",
["entered" /* entered */]: "in",
["exiting" /* exiting */]: "out",
["exited" /* exited */]: "out",
["pre-entering" /* preEntering */]: "out",
["pre-exiting" /* preExiting */]: "out"
};
function getTransitionStyles({
transition,
state,
duration,
timingFunction
}) {
const shared = {
transitionDuration: `${duration}ms`,
transitionTimingFunction: timingFunction
};
if (typeof transition === "string") {
if (!(transition in presets)) {
return {};
}
return {
transitionProperty: presets[transition].transitionProperty,
...shared,
...presets[transition].common,
...presets[transition][transitionStatuses[state]]
};
}
return {
transitionProperty: transition.transitionProperty,
...shared,
...transition.common,
...transition[transitionStatuses[state]]
};
}
// src/global-config.ts
var defaultConfig = {
transition: "fade",
duration: 0.2,
keepMounted: false,
enterDelay: 0,
exitDelay: 0,
timingFunction: "ease",
initial: false,
reduceMotion: false
};
var GlobalConfig = class {
static config = defaultConfig;
/**
* 设置全局配置
*/
static set(props) {
this.config = this.addDefaults(props);
return this.config;
}
/**
* 从全局配置中合并配置,
* 外部配置优先级高于全局配置,
* 并不会改变全局配置
*/
static merge(props) {
const mergedValue = this.addDefaults(props);
return mergedValue;
}
/**
* 给配置添加默认值
*/
static addDefaults(props) {
const value = defaults(props, this.config);
if (value.exitDuration === void 0) {
value.exitDuration = value.duration;
}
return value;
}
static reset() {
this.config = defaultConfig;
}
};
function setGlobalConfig(props) {
GlobalConfig.set.call(GlobalConfig, props);
}
// src/viewport/index.ts
var thresholds = {
some: 0,
all: 1
};
function resolveElements(elements) {
if (typeof elements === "string") {
elements = document.querySelectorAll(elements);
} else if (elements instanceof Element) {
elements = [elements];
}
return Array.from(elements || []);
}
function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) {
const elements = resolveElements(elementOrSelector);
const activeIntersections = /* @__PURE__ */ new WeakMap();
const onIntersectionChange = (entries) => {
entries.forEach((entry) => {
const onEnd = activeIntersections.get(entry.target);
if (entry.isIntersecting === Boolean(onEnd)) return;
if (entry.isIntersecting) {
const newOnEnd = onStart(entry);
if (typeof newOnEnd === "function") {
activeIntersections.set(entry.target, newOnEnd);
} else {
observer.unobserve(entry.target);
}
} else if (onEnd) {
onEnd(entry);
activeIntersections.delete(entry.target);
}
});
};
const observer = new IntersectionObserver(onIntersectionChange, {
root,
rootMargin,
threshold: typeof amount === "number" ? amount : thresholds[amount]
});
elements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
}
// src/hooks/use-in-view.ts
function useInView(ref, { root, margin, amount = "some", once = false } = {}, options = {}) {
const [isInView, setInView] = react.useState(false);
const { enable } = options;
react.useEffect(() => {
if (!enable || !ref.current || once && isInView) return;
const onEnter = () => {
setInView(true);
return once ? void 0 : () => setInView(false);
};
const options2 = {
root: root && root.current || void 0,
margin,
amount
};
return inView(ref.current, onEnter, options2);
}, [root, ref, margin, once, amount]);
return isInView;
}
var Transition = ({
mounted: _mounted,
children,
onExit,
onEntered,
onEnter,
onExited,
viewport,
unsafe_alwaysMounted,
...rest
}) => {
const {
duration,
enterDelay,
exitDelay,
exitDuration,
initial,
keepMounted,
timingFunction,
transition,
reduceMotion
} = GlobalConfig.merge(rest);
const mountedInView = _mounted === "whileInView";
const el = react.useRef(null);
const isInView = useInView(el, viewport, {
enable: mountedInView
});
const mounted = mountedInView ? isInView : _mounted;
const { transitionDuration, transitionStatus, transitionTimingFunction } = useTransition({
mounted,
initial,
exitDuration,
duration,
timingFunction,
onExit,
onEntered,
onEnter,
onExited,
enterDelay,
exitDelay,
reduceMotion
});
const createChildren = useMemoizedFn((style, { mounted: mounted2 }) => {
let element;
if (mounted2 || keepMounted) {
if (typeof children === "function") {
element = children(style);
} else {
if (react.isValidElement(children)) {
element = react.cloneElement(children, { style });
} else {
element = children;
}
}
} else {
element = null;
}
if (!mountedInView) {
return element;
}
const { placeholder, attributes } = viewport || {};
const Placeholder = placeholder || "div";
return /* @__PURE__ */ jsxRuntime.jsx(Placeholder, { ref: el, ...attributes, children: element });
});
const createTransitionChildren = useMemoizedFn(({ mounted: mounted2 }) => {
return createChildren(
getTransitionStyles({
transition,
duration: transitionDuration,
state: transitionStatus,
timingFunction: transitionTimingFunction
}),
{ mounted: mounted2 }
);
});
const isExited = transitionStatus === "exited" /* exited */;
if (isExited) {
if (unsafe_alwaysMounted) {
return createTransitionChildren({ mounted: false });
}
return createChildren({ display: "none" }, { mounted: false });
}
return createTransitionChildren({ mounted: true });
};
exports.Transition = Transition;
exports.presets = presets;
exports.setGlobalConfig = setGlobalConfig;