Files
ant-design/.dumi/hooks/useThemeAnimation.ts
QdabuliuQ c235b1193b site: add keepAlive animation to theme transition fix flickering after switching themes (#56544)
* fix: add keepAlive animation to theme transition

* style: modify animation keyframes

---------

Co-authored-by: thinkasany <480968828@qq.com>
Co-authored-by: 遇见同学 <1875694521@qq.com>
2026-01-12 15:22:52 +08:00

125 lines
3.0 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { removeCSS, updateCSS } from '@rc-component/util/lib/Dom/dynamicCSS';
import theme from '../../components/theme';
const duration = 0.5;
const viewTransitionStyle = `
@keyframes keepAlive {100% { z-index: -1 }}
::view-transition-old(root),
::view-transition-new(root) {
animation: keepAlive ${duration}s linear;
animation-fill-mode: forwards;
mix-blend-mode: normal;
}
.dark::view-transition-old(root) {
z-index: 1;
}
.dark::view-transition-new(root) {
z-index: 999;
}
::view-transition-old(root) {
z-index: 999;
}
::view-transition-new(root) {
z-index: 1;
}
`;
const useThemeAnimation = () => {
const {
token: { colorBgElevated },
} = theme.useToken();
const animateRef = useRef<{ colorBgElevated: string }>({ colorBgElevated });
const startAnimationTheme = (clipPath: string[], isDark: boolean) => {
updateCSS(
`
* {
transition: none !important;
}
`,
'disable-transition',
);
document.documentElement
.animate(
{
clipPath: isDark ? [...clipPath].reverse() : clipPath,
},
{
duration: duration * 1000,
easing: 'ease-in',
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
},
)
.addEventListener('finish', () => {
removeCSS('disable-transition');
});
};
const toggleAnimationTheme = (
event: React.MouseEvent<HTMLElement, MouseEvent>,
isDark: boolean,
) => {
if (!(event && typeof document.startViewTransition === 'function')) {
return;
}
const time = Date.now();
const x = event.clientX;
const y = event.clientY;
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
updateCSS(
`
[data-prefers-color='dark'] {
color-scheme: light !important;
}
[data-prefers-color='light'] {
color-scheme: dark !important;
}
`,
'color-scheme',
);
document
.startViewTransition(async () => {
const root = document.documentElement;
root.classList.remove(isDark ? 'dark' : 'light');
root.classList.add(isDark ? 'light' : 'dark');
})
.ready.then(() => {
// eslint-disable-next-line no-console
console.log(`Theme transition finished in ${Date.now() - time}ms`);
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
];
removeCSS('color-scheme');
startAnimationTheme(clipPath, isDark);
});
};
// inject transition style
useEffect(() => {
if (typeof document.startViewTransition === 'function') {
updateCSS(viewTransitionStyle, 'view-transition-style');
}
}, []);
useEffect(() => {
if (colorBgElevated !== animateRef.current.colorBgElevated) {
animateRef.current.colorBgElevated = colorBgElevated;
}
}, [colorBgElevated]);
return toggleAnimationTheme;
};
export default useThemeAnimation;