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:
@ -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>;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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)" },
|
"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", {
|
||||||
|
Reference in New Issue
Block a user