From 2cf22e8ed3df26d8760c9da2d99c01780bac7b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=A2=E6=9E=9C=E6=B1=81?= Date: Fri, 8 Sep 2023 17:42:12 +0800 Subject: [PATCH] docs: add theme toggle animation (#44655) * docs: add theme toggle animation * fix: add compatibility judgment * refactor: optimization code * fix: server document not found * fix: animation lag * fix: transition issue * fix: scroll bar theme color --- .dumi/hooks/useThemeAnimation.ts | 126 +++++++++++++++++++++++ .dumi/theme/common/ThemeSwitch/index.tsx | 20 ++-- 2 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 .dumi/hooks/useThemeAnimation.ts diff --git a/.dumi/hooks/useThemeAnimation.ts b/.dumi/hooks/useThemeAnimation.ts new file mode 100644 index 000000000000..0ca2049bd536 --- /dev/null +++ b/.dumi/hooks/useThemeAnimation.ts @@ -0,0 +1,126 @@ +import { useEffect, useRef } from 'react'; +import { removeCSS, updateCSS } from 'rc-util/lib/Dom/dynamicCSS'; + +import theme from '../../components/theme'; + +const viewTransitionStyle = ` +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + 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: 500, + easing: 'ease-in', + pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)', + }, + ) + .addEventListener('finish', () => { + removeCSS('disable-transition'); + }); + }; + + const toggleAnimationTheme = (event: MouseEvent, isDark: boolean) => { + // @ts-ignore + if (!(event && typeof document.startViewTransition === 'function')) return; + 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 + // @ts-ignore + .startViewTransition(async () => { + // wait for theme change end + while (colorBgElevated === animateRef.current.colorBgElevated) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 1000 / 60); + }); + } + const root = document.documentElement; + root.classList.remove(isDark ? 'dark' : 'light'); + root.classList.add(isDark ? 'light' : 'dark'); + }) + .ready.then(() => { + 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(() => { + // @ts-ignore + 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; diff --git a/.dumi/theme/common/ThemeSwitch/index.tsx b/.dumi/theme/common/ThemeSwitch/index.tsx index afa8b71da3c7..52ef5178b63f 100644 --- a/.dumi/theme/common/ThemeSwitch/index.tsx +++ b/.dumi/theme/common/ThemeSwitch/index.tsx @@ -1,10 +1,12 @@ +import React from 'react'; import { BgColorsOutlined, SmileOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; +import { useTheme } from 'antd-style'; import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons'; // import { Motion } from 'antd-token-previewer/es/icons'; import { FormattedMessage, Link, useLocation } from 'dumi'; -import React from 'react'; -import { useTheme } from 'antd-style'; -import { FloatButton } from 'antd'; + +import useThemeAnimation from '../../../hooks/useThemeAnimation'; import { getLocalizedPathname, isZhCN } from '../../utils'; import ThemeIcon from './ThemeIcon'; @@ -22,6 +24,9 @@ const ThemeSwitch: React.FC = (props) => { // const isMotionOff = value.includes('motion-off'); const isHappyWork = value.includes('happy-work'); + const isDark = value.includes('dark'); + + const toggleAnimationTheme = useThemeAnimation(); return ( = (props) => { } - type={value.includes('dark') ? 'primary' : 'default'} - onClick={() => { - if (value.includes('dark')) { + type={isDark ? 'primary' : 'default'} + onClick={(e) => { + // Toggle animation when switch theme + toggleAnimationTheme(e, isDark); + + if (isDark) { onChange(value.filter((theme) => theme !== 'dark')); } else { onChange([...value, 'dark']);