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/marquee';
|
||||
export * from './lib/moving-border';
|
||||
export * from './lib/prefers-reduced-motion';
|
||||
export * from './lib/shine-border';
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
})}
|
||||
|
||||
@ -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
|
||||
|
||||
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';
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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, {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user