mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-06-30 06:16:37 -04:00
fix prefers-reduced-motion
detection
This commit is contained in:
@ -67,6 +67,8 @@ const Icon = styled("svg", {
|
||||
const Heart = styled("span", {
|
||||
display: "inline-block",
|
||||
color: "$error", // somewhat ironically color the heart with the themed "error" red... </3
|
||||
|
||||
"@media (prefers-reduced-motion: no-preference)": {
|
||||
animation: `${keyframes({
|
||||
"0%": { transform: "scale(1)" },
|
||||
"2%": { transform: "scale(1.25)" },
|
||||
@ -77,6 +79,7 @@ const Heart = styled("span", {
|
||||
"100%": { transform: "scale(1)" },
|
||||
})} 10s ease 7.5s infinite`,
|
||||
willChange: "transform",
|
||||
},
|
||||
});
|
||||
|
||||
export type FooterProps = ComponentProps<typeof Wrapper>;
|
||||
|
@ -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<ComponentProps<typeof FancyLink>, "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 (
|
||||
<NextLink href={href} prefetch={prefetch} passHref={passHref}>
|
||||
<FancyLink target={target} rel={rel} {...rest} />
|
||||
<FancyLink target={target} rel={rel} underline={underline} {...rest} />
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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,11 +52,9 @@ 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";
|
||||
// listen for changes in OS preference
|
||||
useEffect(() => {
|
||||
const systemTheme = isSystemDark ? "dark" : "light";
|
||||
|
||||
// keep track of the resolved theme whether or not we change it below
|
||||
setResolvedTheme(systemTheme);
|
||||
@ -62,21 +63,7 @@ export const ThemeProvider = ({
|
||||
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);
|
||||
|
||||
// clean up the event listener
|
||||
return () => {
|
||||
media.removeEventListener("change", handleMediaQuery);
|
||||
};
|
||||
}, [handleMediaQuery]);
|
||||
}, [changeTheme, themeNames, preferredTheme, isSystemDark]);
|
||||
|
||||
// color-scheme handling (tells browser how to render built-in elements like forms, scrollbars, etc.)
|
||||
useEffect(() => {
|
||||
|
@ -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];
|
||||
};
|
@ -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;
|
||||
};
|
@ -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)" },
|
||||
"5%": { transform: "rotate(14deg)" },
|
||||
"10%": { transform: "rotate(-8deg)" },
|
||||
@ -77,16 +83,10 @@ const hello = keyframes({
|
||||
"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",
|
||||
})} 5s ease 1s infinite`,
|
||||
transformOrigin: "65% 80%",
|
||||
willChange: "transform",
|
||||
},
|
||||
});
|
||||
|
||||
const Sup = styled("sup", {
|
||||
|
Reference in New Issue
Block a user