feat(nx-dev): honor prefers-reduced-motion (#27541)
Disables animations when browser is set to `prefers-reduced-motion` Fixes #27114
This commit is contained in:
parent
27fe4c6401
commit
d6c3b24eb8
@ -3,4 +3,5 @@ export * from './lib/blur-fade';
|
|||||||
export * from './lib/fit-text';
|
export * from './lib/fit-text';
|
||||||
export * from './lib/marquee';
|
export * from './lib/marquee';
|
||||||
export * from './lib/moving-border';
|
export * from './lib/moving-border';
|
||||||
|
export * from './lib/prefers-reduced-motion';
|
||||||
export * from './lib/shine-border';
|
export * from './lib/shine-border';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { animate, useInView } from 'framer-motion';
|
import { animate, useInView } from 'framer-motion';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { usePrefersReducedMotion } from './prefers-reduced-motion';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animates a value and renders it with a specified suffix.
|
* Animates a value and renders it with a specified suffix.
|
||||||
@ -26,13 +27,14 @@ export function AnimateValue({
|
|||||||
const ref = useRef<HTMLSpanElement | null>(null);
|
const ref = useRef<HTMLSpanElement | null>(null);
|
||||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||||
const isInView = useInView(ref);
|
const isInView = useInView(ref);
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInView) return;
|
if (!isInView) return;
|
||||||
if (isComplete && once) return;
|
if (isComplete && once) return;
|
||||||
|
|
||||||
animate(0, num, {
|
animate(0, num, {
|
||||||
duration: 2.5,
|
duration: shouldReduceMotion ? 0 : 2.5,
|
||||||
onUpdate(value) {
|
onUpdate(value) {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
Variants,
|
Variants,
|
||||||
} from 'framer-motion';
|
} from 'framer-motion';
|
||||||
import { ReactNode, useRef } from 'react';
|
import { ReactNode, useRef } from 'react';
|
||||||
|
import { usePrefersReducedMotion } from './prefers-reduced-motion';
|
||||||
|
|
||||||
interface BlurFadeProps {
|
interface BlurFadeProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -62,6 +63,12 @@ export function BlurFade({
|
|||||||
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
||||||
};
|
};
|
||||||
const combinedVariants = variant || defaultVariants;
|
const combinedVariants = variant || defaultVariants;
|
||||||
|
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
|
if (shouldReduceMotion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { cx } from '@nx/nx-dev/ui-primitives';
|
import { cx } from '@nx/nx-dev/ui-primitives';
|
||||||
|
import { usePrefersReducedMotion } from './prefers-reduced-motion';
|
||||||
|
|
||||||
interface MarqueeProps {
|
interface MarqueeProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -33,6 +34,8 @@ export function Marquee({
|
|||||||
repeat = 4,
|
repeat = 4,
|
||||||
...props
|
...props
|
||||||
}: MarqueeProps) {
|
}: MarqueeProps) {
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
@ -53,6 +56,7 @@ export function Marquee({
|
|||||||
className={cx('flex shrink-0 justify-around [gap:var(--gap)]', {
|
className={cx('flex shrink-0 justify-around [gap:var(--gap)]', {
|
||||||
'animate-marquee flex-row': !vertical,
|
'animate-marquee flex-row': !vertical,
|
||||||
'animate-marquee-vertical flex-col': vertical,
|
'animate-marquee-vertical flex-col': vertical,
|
||||||
|
'[animation-play-state:paused]': shouldReduceMotion,
|
||||||
'group-hover:[animation-play-state:paused]': pauseOnHover,
|
'group-hover:[animation-play-state:paused]': pauseOnHover,
|
||||||
'[animation-direction:reverse]': reverse,
|
'[animation-direction:reverse]': reverse,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
useTransform,
|
useTransform,
|
||||||
} from 'framer-motion';
|
} from 'framer-motion';
|
||||||
import { ReactNode, useRef } from 'react';
|
import { ReactNode, useRef } from 'react';
|
||||||
|
import { usePrefersReducedMotion } from './prefers-reduced-motion';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a moving border effect around the specified component by animating a rectangular SVG element.
|
* Creates a moving border effect around the specified component by animating a rectangular SVG element.
|
||||||
@ -54,6 +55,11 @@ export function MovingBorder({
|
|||||||
|
|
||||||
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
|
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
|
||||||
|
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
|
if (shouldReduceMotion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
23
nx-dev/ui-animations/src/lib/prefers-reduced-motion.tsx
Normal file
23
nx-dev/ui-animations/src/lib/prefers-reduced-motion.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const QUERY = '(prefers-reduced-motion: no-preference)';
|
||||||
|
|
||||||
|
export function usePrefersReducedMotion(): boolean {
|
||||||
|
// Default to no-animations, since we don't know what the
|
||||||
|
// user's preference is on the server.
|
||||||
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQueryList = window.matchMedia(QUERY);
|
||||||
|
// Set the true initial value, now that we're on the client:
|
||||||
|
setPrefersReducedMotion(!window.matchMedia(QUERY).matches);
|
||||||
|
// Register our event listener
|
||||||
|
const listener = (event: any) => {
|
||||||
|
setPrefersReducedMotion(!event.matches);
|
||||||
|
};
|
||||||
|
mediaQueryList.addEventListener('change', listener);
|
||||||
|
return () => {
|
||||||
|
mediaQueryList.removeEventListener('change', listener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return prefersReducedMotion;
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
|
||||||
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@ -48,6 +49,7 @@ export function AgentNumberOverTime(): JSX.Element {
|
|||||||
// Calculate the width of each item
|
// Calculate the width of each item
|
||||||
const itemWidthPercent = remainingPercent / agents.length;
|
const itemWidthPercent = remainingPercent / agents.length;
|
||||||
|
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -58,7 +60,7 @@ export function AgentNumberOverTime(): JSX.Element {
|
|||||||
visible: (i: number) => ({
|
visible: (i: number) => ({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i || 0,
|
delay: shouldReduceMotion ? 0 : i || 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -67,8 +69,8 @@ export function AgentNumberOverTime(): JSX.Element {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i * 0.035,
|
delay: shouldReduceMotion ? 0 : i * 0.035,
|
||||||
duration: 0.65,
|
duration: shouldReduceMotion ? 0 : 0.65,
|
||||||
ease: 'easeOut',
|
ease: 'easeOut',
|
||||||
when: 'beforeChildren',
|
when: 'beforeChildren',
|
||||||
staggerChildren: 0.3,
|
staggerChildren: 0.3,
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import {
|
|||||||
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { NxCloudIcon } from '@nx/nx-dev/ui-icons';
|
import { NxCloudIcon } from '@nx/nx-dev/ui-icons';
|
||||||
|
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
|
||||||
|
|
||||||
export function AutomatedAgentsManagement(): JSX.Element {
|
export function AutomatedAgentsManagement(): JSX.Element {
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -20,7 +22,7 @@ export function AutomatedAgentsManagement(): JSX.Element {
|
|||||||
visible: (i: number) => ({
|
visible: (i: number) => ({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i || 0,
|
delay: shouldReduceMotion ? 0 : i || 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -29,8 +31,8 @@ export function AutomatedAgentsManagement(): JSX.Element {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i * 0.035,
|
delay: shouldReduceMotion ? 0 : i * 0.035,
|
||||||
duration: 0.65,
|
duration: shouldReduceMotion ? 0 : 0.65,
|
||||||
ease: 'easeOut',
|
ease: 'easeOut',
|
||||||
when: 'beforeChildren',
|
when: 'beforeChildren',
|
||||||
staggerChildren: 0.3,
|
staggerChildren: 0.3,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
|
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the total number of years worth of compute.
|
* Calculate the total number of years worth of compute.
|
||||||
@ -71,6 +72,8 @@ const stats = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function Statistics(): JSX.Element {
|
export function Statistics(): JSX.Element {
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -81,7 +84,7 @@ export function Statistics(): JSX.Element {
|
|||||||
visible: (i: number) => ({
|
visible: (i: number) => ({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i || 0,
|
delay: shouldReduceMotion ? 0 : i || 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -90,8 +93,8 @@ export function Statistics(): JSX.Element {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i * 0.25,
|
delay: shouldReduceMotion ? 0 : i * 0.25,
|
||||||
duration: 0.65,
|
duration: shouldReduceMotion ? 0 : 0.65,
|
||||||
ease: 'easeOut',
|
ease: 'easeOut',
|
||||||
when: 'beforeChildren',
|
when: 'beforeChildren',
|
||||||
staggerChildren: 0.3,
|
staggerChildren: 0.3,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { BentoGrid, BentoGridItem } from './elements/bento-grid';
|
|||||||
import { cx } from '@nx/nx-dev/ui-primitives';
|
import { cx } from '@nx/nx-dev/ui-primitives';
|
||||||
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
|
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
|
||||||
|
|
||||||
export function UnderstandWorkspace(): JSX.Element {
|
export function UnderstandWorkspace(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@ -155,6 +156,7 @@ const Caching = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FlakyTasks = () => {
|
const FlakyTasks = () => {
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -171,8 +173,8 @@ const FlakyTasks = () => {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
x: 0,
|
x: 0,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i * 0.2,
|
delay: shouldReduceMotion ? 0 : i * 0.2,
|
||||||
duration: 0.275,
|
duration: shouldReduceMotion ? 0 : 0.275,
|
||||||
ease: 'easeOut',
|
ease: 'easeOut',
|
||||||
when: 'beforeChildren',
|
when: 'beforeChildren',
|
||||||
staggerChildren: 0.3,
|
staggerChildren: 0.3,
|
||||||
@ -439,6 +441,7 @@ const SplitE2eTests = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TaskDistribution = () => {
|
const TaskDistribution = () => {
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -455,8 +458,8 @@ const TaskDistribution = () => {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
x: 0,
|
x: 0,
|
||||||
transition: {
|
transition: {
|
||||||
delay: i * 0.2,
|
delay: shouldReduceMotion ? 0 : i * 0.2,
|
||||||
duration: 0.275,
|
duration: shouldReduceMotion ? 0 : 0.275,
|
||||||
ease: 'easeOut',
|
ease: 'easeOut',
|
||||||
when: 'beforeChildren',
|
when: 'beforeChildren',
|
||||||
staggerChildren: 0.3,
|
staggerChildren: 0.3,
|
||||||
@ -745,6 +748,10 @@ export function Counter({
|
|||||||
}) {
|
}) {
|
||||||
const count = useMotionValue(0);
|
const count = useMotionValue(0);
|
||||||
const rounded = useTransform(count, Math.round);
|
const rounded = useTransform(count, Math.round);
|
||||||
|
const shouldReduceMotion = usePrefersReducedMotion();
|
||||||
|
if (shouldReduceMotion) {
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const animation = animate(count, value, {
|
const animation = animate(count, value, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user