From 2ea6495fd88a15331ac6bb5e29c06b966621ebaa Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Tue, 3 May 2022 14:57:32 -0400 Subject: [PATCH] fix `prefers-reduced-motion` detection --- components/Footer/Footer.tsx | 23 ++++++++------- components/Link/Link.tsx | 21 ++++++++------ components/ThemeToggle/ThemeToggle.tsx | 6 ++-- contexts/ThemeContext.tsx | 39 +++++++++----------------- hooks/use-local-storage.ts | 22 --------------- hooks/use-prefers-reduced-motion.ts | 31 -------------------- pages/index.tsx | 32 ++++++++++----------- 7 files changed, 57 insertions(+), 117 deletions(-) delete mode 100644 hooks/use-local-storage.ts delete mode 100644 hooks/use-prefers-reduced-motion.ts diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx index b68a9b2c..99da7a94 100644 --- a/components/Footer/Footer.tsx +++ b/components/Footer/Footer.tsx @@ -67,16 +67,19 @@ const Icon = styled("svg", { const Heart = styled("span", { display: "inline-block", color: "$error", // somewhat ironically color the heart with the themed "error" red... ; diff --git a/components/Link/Link.tsx b/components/Link/Link.tsx index 6d08e3fa..f6799970 100644 --- a/components/Link/Link.tsx +++ b/components/Link/Link.tsx @@ -9,33 +9,34 @@ const FancyLink = styled("a", { textDecoration: "none", variants: { - // fancy animated link underline effect underline: { + // fancy animated link underline effect (on by default) true: { - // sets psuedo linear-gradient() for the underline's color; see stitches config for the weird calculation behind the - // local `$$underline` variable. + // sets psuedo linear-gradient() for the underline's color; see stitches config for the weird calculation behind + // the local `$$underline` variable. setUnderlineVar: { color: "$colors$linkUnderline" }, backgroundImage: `linear-gradient($$underline, $$underline)`, backgroundPosition: "0% 100%", backgroundRepeat: "no-repeat", backgroundSize: "0% $underline", - transition: "background-size 0.25s ease-in-out", paddingBottom: "0.2rem", + "@media (prefers-reduced-motion: no-preference)": { + transition: "background-size 0.25s ease-in-out", + }, + "&:hover": { backgroundSize: "100% $underline", }, }, + false: {}, }, }, - - defaultVariants: { - underline: true, - }, }); export type CustomLinkProps = Omit, "href"> & NextLinkProps & { + underline?: boolean; forceNewWindow?: boolean; }; @@ -45,6 +46,7 @@ const CustomLink = ({ passHref = true, target, rel, + underline = true, forceNewWindow, ...rest }: CustomLinkProps) => { @@ -58,13 +60,14 @@ const CustomLink = ({ href={href.toString()} target={target ?? "_blank"} rel={[rel, "noopener", isExternal ? "noreferrer" : ""].join(" ").trim()} + underline={underline} {...rest} /> ); } else { return ( - + ); } diff --git a/components/ThemeToggle/ThemeToggle.tsx b/components/ThemeToggle/ThemeToggle.tsx index e0c289ad..138e9590 100644 --- a/components/ThemeToggle/ThemeToggle.tsx +++ b/components/ThemeToggle/ThemeToggle.tsx @@ -1,8 +1,8 @@ import { useEffect, memo } from "react"; +import { useMedia } from "react-use"; import { useSpring, animated, Globals } from "@react-spring/web"; import { useTheme } from "../../hooks/use-theme"; import { useHasMounted } from "../../hooks/use-has-mounted"; -import { usePrefersReducedMotion } from "../../hooks/use-prefers-reduced-motion"; import { styled } from "../../lib/styles/stitches.config"; const Button = styled("button", { @@ -25,7 +25,7 @@ export type ThemeToggleProps = { const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => { const hasMounted = useHasMounted(); - const prefersReducedMotion = usePrefersReducedMotion(); + const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false); const { resolvedTheme, setTheme } = useTheme(); // default to light since `resolvedTheme` might be undefined @@ -34,7 +34,7 @@ const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => { // accessibility: skip animation if user prefers reduced motion useEffect(() => { Globals.assign({ - skipAnimation: prefersReducedMotion, + skipAnimation: !!prefersReducedMotion, }); }, [prefersReducedMotion]); diff --git a/contexts/ThemeContext.tsx b/contexts/ThemeContext.tsx index dfb5b32f..20f2711f 100644 --- a/contexts/ThemeContext.tsx +++ b/contexts/ThemeContext.tsx @@ -1,5 +1,5 @@ import { createContext, useCallback, useEffect, useState } from "react"; -import { useLocalStorage } from "react-use"; +import { useLocalStorage, useMedia } from "react-use"; import { darkModeQuery, themeStorageKey } from "../lib/config/themes"; import type { Context, PropsWithChildren } from "react"; @@ -31,6 +31,9 @@ export const ThemeProvider = ({ const [preferredTheme, setPreferredTheme] = useLocalStorage(themeStorageKey, null, { raw: true }); // save the end result no matter how we got there (by preference or by system): const [resolvedTheme, setResolvedTheme] = useState(""); + // hook into system `prefers-dark-mode` setting + const isSystemDark = useMedia(darkModeQuery, false); + // get the theme names (light, dark) via passed-in classnames' keys const themeNames = Object.keys(classNames); @@ -49,34 +52,18 @@ export const ThemeProvider = ({ [classNames, setPreferredTheme] ); - // memoize browser media query handler - const handleMediaQuery = useCallback( - (e: MediaQueryListEvent | MediaQueryList) => { - // get the user's preferred theme via their OS/browser settings - const systemTheme = e.matches ? "dark" : "light"; - - // keep track of the resolved theme whether or not we change it below - setResolvedTheme(systemTheme); - - // only actually change the theme if preference is unset (and *don't* save new theme to storage) - if (!preferredTheme || !themeNames.includes(preferredTheme)) { - changeTheme(systemTheme, false); - } - }, - [changeTheme, preferredTheme, themeNames] - ); - // listen for changes in OS preference useEffect(() => { - const media = window.matchMedia(darkModeQuery); - media.addEventListener("change", handleMediaQuery); - handleMediaQuery(media); + const systemTheme = isSystemDark ? "dark" : "light"; - // clean up the event listener - return () => { - media.removeEventListener("change", handleMediaQuery); - }; - }, [handleMediaQuery]); + // keep track of the resolved theme whether or not we change it below + setResolvedTheme(systemTheme); + + // only actually change the theme if preference is unset (and *don't* save new theme to storage) + if (!preferredTheme || !themeNames.includes(preferredTheme)) { + changeTheme(systemTheme, false); + } + }, [changeTheme, themeNames, preferredTheme, isSystemDark]); // color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.) useEffect(() => { diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts deleted file mode 100644 index 14c1fc4b..00000000 --- a/hooks/use-local-storage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useState, useEffect } from "react"; - -export const useLocalStorage = (key: string, allowNull = false) => { - const [value, setValue] = useState(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let currentValue: any; - try { - currentValue = window.localStorage.getItem(key); - } catch (error) {} // eslint-disable-line no-empty - - return currentValue; - }); - - useEffect(() => { - // don't write null values to storage unless specified - if (value !== null || allowNull) { - window.localStorage?.setItem(key, value); - } - }, [key, value, allowNull]); - - return [value, setValue]; -}; diff --git a/hooks/use-prefers-reduced-motion.ts b/hooks/use-prefers-reduced-motion.ts deleted file mode 100644 index 59297df6..00000000 --- a/hooks/use-prefers-reduced-motion.ts +++ /dev/null @@ -1,31 +0,0 @@ -// SSR-safe reduced motion hook: -// https://www.joshwcomeau.com/react/prefers-reduced-motion/#ssr-safety - -import { useEffect, useState } from "react"; - -const QUERY = "(prefers-reduced-motion: no-preference)"; - -export const usePrefersReducedMotion = (): boolean => { - // default to no animations on server-side - const [prefersReducedMotion, setPrefersReducedMotion] = useState(true); - - useEffect(() => { - // this can now be safely set for the first time on the client-side - setPrefersReducedMotion(!window.matchMedia(QUERY).matches); - - // register a listener for changes - const listener = (event: MediaQueryListEvent) => { - setPrefersReducedMotion(!event.matches); - }; - - const mediaQueryList = window.matchMedia(QUERY); - mediaQueryList.addEventListener("change", listener); - - // clean up the event listener - return () => { - mediaQueryList.removeEventListener("change", listener); - }; - }, [setPrefersReducedMotion]); - - return prefersReducedMotion; -}; diff --git a/pages/index.tsx b/pages/index.tsx index c06b78fd..9c15b789 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -67,26 +67,26 @@ const Paragraph = styled("p", { }, }); -const hello = keyframes({ - "0%": { transform: "rotate(0deg)" }, - "5%": { transform: "rotate(14deg)" }, - "10%": { transform: "rotate(-8deg)" }, - "15%": { transform: "rotate(14deg)" }, - "20%": { transform: "rotate(-4deg)" }, - "25%": { transform: "rotate(10deg)" }, - "30%": { transform: "rotate(0deg)" }, - // pause for ~9 out of 10 seconds - "100%": { transform: "rotate(0deg)" }, -}); - const Wave = styled("span", { display: "inline-block", marginLeft: "0.1em", fontSize: "1.2em", - animation: `${hello} 5s infinite`, - animationDelay: "1s", - transformOrigin: "65% 80%", - willChange: "transform", + + "@media (prefers-reduced-motion: no-preference)": { + animation: `${keyframes({ + "0%": { transform: "rotate(0deg)" }, + "5%": { transform: "rotate(14deg)" }, + "10%": { transform: "rotate(-8deg)" }, + "15%": { transform: "rotate(14deg)" }, + "20%": { transform: "rotate(-4deg)" }, + "25%": { transform: "rotate(10deg)" }, + "30%": { transform: "rotate(0deg)" }, + // pause for ~9 out of 10 seconds + "100%": { transform: "rotate(0deg)" }, + })} 5s ease 1s infinite`, + transformOrigin: "65% 80%", + willChange: "transform", + }, }); const Sup = styled("sup", {