From ef8d3924b90a93bd287ce11810213ec387861a63 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Fri, 3 Jun 2022 17:32:44 +0800 Subject: [PATCH] client: improve theme support set dark, light and follow system --- .../client/src/components/theme/index.tsx | 52 +++++++++--- packages/client/src/hooks/use-theme.tsx | 85 ++++++++++++++----- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/packages/client/src/components/theme/index.tsx b/packages/client/src/components/theme/index.tsx index 88f3e9f..a8b656d 100644 --- a/packages/client/src/components/theme/index.tsx +++ b/packages/client/src/components/theme/index.tsx @@ -1,17 +1,47 @@ -import { IconMoon, IconSun } from '@douyinfe/semi-icons'; -import { Button } from '@douyinfe/semi-ui'; -import { Tooltip } from 'components/tooltip'; -import { Theme as ThemeState } from 'hooks/use-theme'; -import React from 'react'; +import { IconDesktop, IconMoon, IconSun } from '@douyinfe/semi-icons'; +import { Button, Dropdown } from '@douyinfe/semi-ui'; +import { Theme as ThemeState, ThemeEnum } from 'hooks/use-theme'; +import React, { useCallback } from 'react'; export const Theme = () => { - const { theme, toggle } = ThemeState.useHook(); - const Icon = theme === 'dark' ? IconSun : IconMoon; - const text = theme === 'dark' ? '切换到亮色模式' : '切换到深色模式'; + const { userPrefer, toggle } = ThemeState.useHook(); + const Icon = userPrefer === 'dark' ? IconSun : IconMoon; + + const setLight = useCallback(() => { + toggle(ThemeEnum.light); + }, [toggle]); + + const setDark = useCallback(() => { + toggle(ThemeEnum.dark); + }, [toggle]); + + const setSystem = useCallback(() => { + toggle(ThemeEnum.system); + }, [toggle]); return ( - - - + + + + 亮色 + + + + 夜色 + + + + 系统 + + + } + > + + ); }; diff --git a/packages/client/src/hooks/use-theme.tsx b/packages/client/src/hooks/use-theme.tsx index 573d938..5759e37 100644 --- a/packages/client/src/hooks/use-theme.tsx +++ b/packages/client/src/hooks/use-theme.tsx @@ -1,33 +1,31 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { createGlobalHook } from './create-global-hook'; export enum ThemeEnum { 'dark' = 'dark', 'light' = 'light', + 'system' = 'system', +} + +function syncSystemTheme() { + const mql = window.matchMedia('(prefers-color-scheme: dark)'); + function matchMode(e) { + if (e.matches) { + document.body.setAttribute('theme-mode', 'dark'); + } else { + document.body.setAttribute('theme-mode', 'light'); + } + } + matchMode(mql); } const useThemeHook = () => { - const [theme, setTheme] = useState(ThemeEnum.light); + const $remove = useRef<() => void>(); + const [theme, setTheme] = useState(ThemeEnum.system); + const [userPrefer, setUserPrefer] = useState(ThemeEnum.system); - const toggle = useCallback(() => { - const nextTheme = theme === 'dark' ? ThemeEnum.light : ThemeEnum.dark; - setTheme(nextTheme); - }, [theme]); - - useEffect(() => { - const body = document.body; - if (theme === 'dark') { - body.setAttribute('theme-mode', 'dark'); - return; - } - if (theme === 'light') { - body.setAttribute('theme-mode', 'light'); - return; - } - }, [theme]); - - useEffect(() => { + const followSystem = useCallback(() => { const mql = window.matchMedia('(prefers-color-scheme: dark)'); function matchMode(e) { @@ -41,15 +39,58 @@ const useThemeHook = () => { matchMode(mql); mql.addEventListener('change', matchMode); - return () => { + const remove = () => { mql.removeEventListener('change', matchMode); }; + + $remove.current = remove; + }, []); + + const toggle = useCallback( + (nextTheme: ThemeEnum) => { + setUserPrefer(nextTheme); + setTheme(nextTheme); + if (nextTheme !== ThemeEnum.system) { + $remove.current && $remove.current(); + } else { + followSystem(); + } + }, + [followSystem] + ); + + useEffect(() => { + const body = document.body; + + switch (theme) { + case ThemeEnum.light: + body.setAttribute('theme-mode', 'light'); + return; + + case ThemeEnum.dark: + body.setAttribute('theme-mode', 'dark'); + return; + + case ThemeEnum.system: + default: + syncSystemTheme(); + return; + } + }, [theme]); + + useEffect(() => { + if (theme === ThemeEnum.system) { + followSystem(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { + userPrefer, theme, toggle, }; }; -export const Theme = createGlobalHook<{ theme: ThemeEnum; toggle: () => void }>(useThemeHook); +export const Theme = + createGlobalHook<{ userPrefer: ThemeEnum; theme: ThemeEnum; toggle: (nextTheme: ThemeEnum) => void }>(useThemeHook);