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:
Isaac Mann 2024-08-20 11:25:29 -04:00 committed by GitHub
parent 27fe4c6401
commit d6c3b24eb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 71 additions and 14 deletions

View File

@ -3,4 +3,5 @@ export * from './lib/blur-fade';
export * from './lib/fit-text';
export * from './lib/marquee';
export * from './lib/moving-border';
export * from './lib/prefers-reduced-motion';
export * from './lib/shine-border';

View File

@ -1,5 +1,6 @@
import { animate, useInView } from 'framer-motion';
import { useEffect, useRef, useState } from 'react';
import { usePrefersReducedMotion } from './prefers-reduced-motion';
/**
* Animates a value and renders it with a specified suffix.
@ -26,13 +27,14 @@ export function AnimateValue({
const ref = useRef<HTMLSpanElement | null>(null);
const [isComplete, setIsComplete] = useState<boolean>(false);
const isInView = useInView(ref);
const shouldReduceMotion = usePrefersReducedMotion();
useEffect(() => {
if (!isInView) return;
if (isComplete && once) return;
animate(0, num, {
duration: 2.5,
duration: shouldReduceMotion ? 0 : 2.5,
onUpdate(value) {
if (!ref.current) return;

View File

@ -7,6 +7,7 @@ import {
Variants,
} from 'framer-motion';
import { ReactNode, useRef } from 'react';
import { usePrefersReducedMotion } from './prefers-reduced-motion';
interface BlurFadeProps {
children: ReactNode;
@ -62,6 +63,12 @@ export function BlurFade({
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
};
const combinedVariants = variant || defaultVariants;
const shouldReduceMotion = usePrefersReducedMotion();
if (shouldReduceMotion) {
return;
}
return (
<AnimatePresence>
<motion.div

View File

@ -1,4 +1,5 @@
import { cx } from '@nx/nx-dev/ui-primitives';
import { usePrefersReducedMotion } from './prefers-reduced-motion';
interface MarqueeProps {
className?: string;
@ -33,6 +34,8 @@ export function Marquee({
repeat = 4,
...props
}: MarqueeProps) {
const shouldReduceMotion = usePrefersReducedMotion();
return (
<div
{...props}
@ -53,6 +56,7 @@ export function Marquee({
className={cx('flex shrink-0 justify-around [gap:var(--gap)]', {
'animate-marquee flex-row': !vertical,
'animate-marquee-vertical flex-col': vertical,
'[animation-play-state:paused]': shouldReduceMotion,
'group-hover:[animation-play-state:paused]': pauseOnHover,
'[animation-direction:reverse]': reverse,
})}

View File

@ -6,6 +6,7 @@ import {
useTransform,
} from 'framer-motion';
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.
@ -54,6 +55,11 @@ export function MovingBorder({
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
const shouldReduceMotion = usePrefersReducedMotion();
if (shouldReduceMotion) {
return;
}
return (
<>
<svg

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

View File

@ -1,4 +1,5 @@
'use client';
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
import { SectionHeading } from '@nx/nx-dev/ui-common';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
@ -48,6 +49,7 @@ export function AgentNumberOverTime(): JSX.Element {
// Calculate the width of each item
const itemWidthPercent = remainingPercent / agents.length;
const shouldReduceMotion = usePrefersReducedMotion();
const variants = {
hidden: {
opacity: 0,
@ -58,7 +60,7 @@ export function AgentNumberOverTime(): JSX.Element {
visible: (i: number) => ({
opacity: 1,
transition: {
delay: i || 0,
delay: shouldReduceMotion ? 0 : i || 0,
},
}),
};
@ -67,8 +69,8 @@ export function AgentNumberOverTime(): JSX.Element {
opacity: 1,
y: 0,
transition: {
delay: i * 0.035,
duration: 0.65,
delay: shouldReduceMotion ? 0 : i * 0.035,
duration: shouldReduceMotion ? 0 : 0.65,
ease: 'easeOut',
when: 'beforeChildren',
staggerChildren: 0.3,

View File

@ -8,8 +8,10 @@ import {
import { SectionHeading } from '@nx/nx-dev/ui-common';
import { motion } from 'framer-motion';
import { NxCloudIcon } from '@nx/nx-dev/ui-icons';
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
export function AutomatedAgentsManagement(): JSX.Element {
const shouldReduceMotion = usePrefersReducedMotion();
const variants = {
hidden: {
opacity: 0,
@ -20,7 +22,7 @@ export function AutomatedAgentsManagement(): JSX.Element {
visible: (i: number) => ({
opacity: 1,
transition: {
delay: i || 0,
delay: shouldReduceMotion ? 0 : i || 0,
},
}),
};
@ -29,8 +31,8 @@ export function AutomatedAgentsManagement(): JSX.Element {
opacity: 1,
y: 0,
transition: {
delay: i * 0.035,
duration: 0.65,
delay: shouldReduceMotion ? 0 : i * 0.035,
duration: shouldReduceMotion ? 0 : 0.65,
ease: 'easeOut',
when: 'beforeChildren',
staggerChildren: 0.3,

View File

@ -2,6 +2,7 @@
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
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.
@ -71,6 +72,8 @@ const stats = [
];
export function Statistics(): JSX.Element {
const shouldReduceMotion = usePrefersReducedMotion();
const variants = {
hidden: {
opacity: 0,
@ -81,7 +84,7 @@ export function Statistics(): JSX.Element {
visible: (i: number) => ({
opacity: 1,
transition: {
delay: i || 0,
delay: shouldReduceMotion ? 0 : i || 0,
},
}),
};
@ -90,8 +93,8 @@ export function Statistics(): JSX.Element {
opacity: 1,
y: 0,
transition: {
delay: i * 0.25,
duration: 0.65,
delay: shouldReduceMotion ? 0 : i * 0.25,
duration: shouldReduceMotion ? 0 : 0.65,
ease: 'easeOut',
when: 'beforeChildren',
staggerChildren: 0.3,

View File

@ -14,6 +14,7 @@ import { BentoGrid, BentoGridItem } from './elements/bento-grid';
import { cx } from '@nx/nx-dev/ui-primitives';
import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
import { useEffect } from 'react';
import { usePrefersReducedMotion } from '@nx/nx-dev/ui-animations';
export function UnderstandWorkspace(): JSX.Element {
return (
@ -155,6 +156,7 @@ const Caching = () => {
};
const FlakyTasks = () => {
const shouldReduceMotion = usePrefersReducedMotion();
const variants = {
hidden: {
opacity: 0,
@ -171,8 +173,8 @@ const FlakyTasks = () => {
opacity: 1,
x: 0,
transition: {
delay: i * 0.2,
duration: 0.275,
delay: shouldReduceMotion ? 0 : i * 0.2,
duration: shouldReduceMotion ? 0 : 0.275,
ease: 'easeOut',
when: 'beforeChildren',
staggerChildren: 0.3,
@ -439,6 +441,7 @@ const SplitE2eTests = () => {
};
const TaskDistribution = () => {
const shouldReduceMotion = usePrefersReducedMotion();
const variants = {
hidden: {
opacity: 0,
@ -455,8 +458,8 @@ const TaskDistribution = () => {
opacity: 1,
x: 0,
transition: {
delay: i * 0.2,
duration: 0.275,
delay: shouldReduceMotion ? 0 : i * 0.2,
duration: shouldReduceMotion ? 0 : 0.275,
ease: 'easeOut',
when: 'beforeChildren',
staggerChildren: 0.3,
@ -745,6 +748,10 @@ export function Counter({
}) {
const count = useMotionValue(0);
const rounded = useTransform(count, Math.round);
const shouldReduceMotion = usePrefersReducedMotion();
if (shouldReduceMotion) {
duration = 0;
}
useEffect(() => {
const animation = animate(count, value, {