1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-30 11:16:38 -04:00

fix prefers-reduced-motion detection

This commit is contained in:
2022-05-03 14:57:32 -04:00
parent bb4115a529
commit 2ea6495fd8
7 changed files with 57 additions and 117 deletions

View File

@ -67,6 +67,8 @@ const Icon = styled("svg", {
const Heart = styled("span", { const Heart = styled("span", {
display: "inline-block", display: "inline-block",
color: "$error", // somewhat ironically color the heart with the themed "error" red... </3 color: "$error", // somewhat ironically color the heart with the themed "error" red... </3
"@media (prefers-reduced-motion: no-preference)": {
animation: `${keyframes({ animation: `${keyframes({
"0%": { transform: "scale(1)" }, "0%": { transform: "scale(1)" },
"2%": { transform: "scale(1.25)" }, "2%": { transform: "scale(1.25)" },
@ -77,6 +79,7 @@ const Heart = styled("span", {
"100%": { transform: "scale(1)" }, "100%": { transform: "scale(1)" },
})} 10s ease 7.5s infinite`, })} 10s ease 7.5s infinite`,
willChange: "transform", willChange: "transform",
},
}); });
export type FooterProps = ComponentProps<typeof Wrapper>; export type FooterProps = ComponentProps<typeof Wrapper>;

View File

@ -9,33 +9,34 @@ const FancyLink = styled("a", {
textDecoration: "none", textDecoration: "none",
variants: { variants: {
// fancy animated link underline effect
underline: { underline: {
// fancy animated link underline effect (on by default)
true: { true: {
// sets psuedo linear-gradient() for the underline's color; see stitches config for the weird calculation behind the // sets psuedo linear-gradient() for the underline's color; see stitches config for the weird calculation behind
// local `$$underline` variable. // the local `$$underline` variable.
setUnderlineVar: { color: "$colors$linkUnderline" }, setUnderlineVar: { color: "$colors$linkUnderline" },
backgroundImage: `linear-gradient($$underline, $$underline)`, backgroundImage: `linear-gradient($$underline, $$underline)`,
backgroundPosition: "0% 100%", backgroundPosition: "0% 100%",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: "0% $underline", backgroundSize: "0% $underline",
transition: "background-size 0.25s ease-in-out",
paddingBottom: "0.2rem", paddingBottom: "0.2rem",
"@media (prefers-reduced-motion: no-preference)": {
transition: "background-size 0.25s ease-in-out",
},
"&:hover": { "&:hover": {
backgroundSize: "100% $underline", backgroundSize: "100% $underline",
}, },
}, },
false: {},
}, },
}, },
defaultVariants: {
underline: true,
},
}); });
export type CustomLinkProps = Omit<ComponentProps<typeof FancyLink>, "href"> & export type CustomLinkProps = Omit<ComponentProps<typeof FancyLink>, "href"> &
NextLinkProps & { NextLinkProps & {
underline?: boolean;
forceNewWindow?: boolean; forceNewWindow?: boolean;
}; };
@ -45,6 +46,7 @@ const CustomLink = ({
passHref = true, passHref = true,
target, target,
rel, rel,
underline = true,
forceNewWindow, forceNewWindow,
...rest ...rest
}: CustomLinkProps) => { }: CustomLinkProps) => {
@ -58,13 +60,14 @@ const CustomLink = ({
href={href.toString()} href={href.toString()}
target={target ?? "_blank"} target={target ?? "_blank"}
rel={[rel, "noopener", isExternal ? "noreferrer" : ""].join(" ").trim()} rel={[rel, "noopener", isExternal ? "noreferrer" : ""].join(" ").trim()}
underline={underline}
{...rest} {...rest}
/> />
); );
} else { } else {
return ( return (
<NextLink href={href} prefetch={prefetch} passHref={passHref}> <NextLink href={href} prefetch={prefetch} passHref={passHref}>
<FancyLink target={target} rel={rel} {...rest} /> <FancyLink target={target} rel={rel} underline={underline} {...rest} />
</NextLink> </NextLink>
); );
} }

View File

@ -1,8 +1,8 @@
import { useEffect, memo } from "react"; import { useEffect, memo } from "react";
import { useMedia } from "react-use";
import { useSpring, animated, Globals } from "@react-spring/web"; import { useSpring, animated, Globals } from "@react-spring/web";
import { useTheme } from "../../hooks/use-theme"; import { useTheme } from "../../hooks/use-theme";
import { useHasMounted } from "../../hooks/use-has-mounted"; import { useHasMounted } from "../../hooks/use-has-mounted";
import { usePrefersReducedMotion } from "../../hooks/use-prefers-reduced-motion";
import { styled } from "../../lib/styles/stitches.config"; import { styled } from "../../lib/styles/stitches.config";
const Button = styled("button", { const Button = styled("button", {
@ -25,7 +25,7 @@ export type ThemeToggleProps = {
const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => { const ThemeToggle = ({ id = "nav", className }: ThemeToggleProps) => {
const hasMounted = useHasMounted(); const hasMounted = useHasMounted();
const prefersReducedMotion = usePrefersReducedMotion(); const prefersReducedMotion = useMedia("(prefers-reduced-motion: reduce)", false);
const { resolvedTheme, setTheme } = useTheme(); const { resolvedTheme, setTheme } = useTheme();
// default to light since `resolvedTheme` might be undefined // 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 // accessibility: skip animation if user prefers reduced motion
useEffect(() => { useEffect(() => {
Globals.assign({ Globals.assign({
skipAnimation: prefersReducedMotion, skipAnimation: !!prefersReducedMotion,
}); });
}, [prefersReducedMotion]); }, [prefersReducedMotion]);

View File

@ -1,5 +1,5 @@
import { createContext, useCallback, useEffect, useState } from "react"; 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 { darkModeQuery, themeStorageKey } from "../lib/config/themes";
import type { Context, PropsWithChildren } from "react"; import type { Context, PropsWithChildren } from "react";
@ -31,6 +31,9 @@ export const ThemeProvider = ({
const [preferredTheme, setPreferredTheme] = useLocalStorage(themeStorageKey, null, { raw: true }); const [preferredTheme, setPreferredTheme] = useLocalStorage(themeStorageKey, null, { raw: true });
// save the end result no matter how we got there (by preference or by system): // save the end result no matter how we got there (by preference or by system):
const [resolvedTheme, setResolvedTheme] = useState(""); 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 // get the theme names (light, dark) via passed-in classnames' keys
const themeNames = Object.keys(classNames); const themeNames = Object.keys(classNames);
@ -49,11 +52,9 @@ export const ThemeProvider = ({
[classNames, setPreferredTheme] [classNames, setPreferredTheme]
); );
// memoize browser media query handler // listen for changes in OS preference
const handleMediaQuery = useCallback( useEffect(() => {
(e: MediaQueryListEvent | MediaQueryList) => { const systemTheme = isSystemDark ? "dark" : "light";
// 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 // keep track of the resolved theme whether or not we change it below
setResolvedTheme(systemTheme); setResolvedTheme(systemTheme);
@ -62,21 +63,7 @@ export const ThemeProvider = ({
if (!preferredTheme || !themeNames.includes(preferredTheme)) { if (!preferredTheme || !themeNames.includes(preferredTheme)) {
changeTheme(systemTheme, false); changeTheme(systemTheme, false);
} }
}, }, [changeTheme, themeNames, preferredTheme, isSystemDark]);
[changeTheme, preferredTheme, themeNames]
);
// listen for changes in OS preference
useEffect(() => {
const media = window.matchMedia(darkModeQuery);
media.addEventListener("change", handleMediaQuery);
handleMediaQuery(media);
// clean up the event listener
return () => {
media.removeEventListener("change", handleMediaQuery);
};
}, [handleMediaQuery]);
// color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.) // color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.)
useEffect(() => { useEffect(() => {

View File

@ -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];
};

View File

@ -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;
};

View File

@ -67,7 +67,13 @@ const Paragraph = styled("p", {
}, },
}); });
const hello = keyframes({ const Wave = styled("span", {
display: "inline-block",
marginLeft: "0.1em",
fontSize: "1.2em",
"@media (prefers-reduced-motion: no-preference)": {
animation: `${keyframes({
"0%": { transform: "rotate(0deg)" }, "0%": { transform: "rotate(0deg)" },
"5%": { transform: "rotate(14deg)" }, "5%": { transform: "rotate(14deg)" },
"10%": { transform: "rotate(-8deg)" }, "10%": { transform: "rotate(-8deg)" },
@ -77,16 +83,10 @@ const hello = keyframes({
"30%": { transform: "rotate(0deg)" }, "30%": { transform: "rotate(0deg)" },
// pause for ~9 out of 10 seconds // pause for ~9 out of 10 seconds
"100%": { transform: "rotate(0deg)" }, "100%": { transform: "rotate(0deg)" },
}); })} 5s ease 1s infinite`,
const Wave = styled("span", {
display: "inline-block",
marginLeft: "0.1em",
fontSize: "1.2em",
animation: `${hello} 5s infinite`,
animationDelay: "1s",
transformOrigin: "65% 80%", transformOrigin: "65% 80%",
willChange: "transform", willChange: "transform",
},
}); });
const Sup = styled("sup", { const Sup = styled("sup", {