diff --git a/ts/components/AnimatedEmojiGalore.tsx b/ts/components/AnimatedEmojiGalore.tsx index 1d9a3620f7..dbb4e49fdf 100644 --- a/ts/components/AnimatedEmojiGalore.tsx +++ b/ts/components/AnimatedEmojiGalore.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { animated, to as interpolate, useSprings } from '@react-spring/web'; import { random } from 'lodash'; import { Emojify } from './conversation/Emojify'; +import { useReducedMotion } from '../hooks/useReducedMotion'; export type PropsType = { emoji: string; @@ -38,9 +39,11 @@ export function AnimatedEmojiGalore({ emoji, onAnimationEnd, }: PropsType): JSX.Element { + const reducedMotion = useReducedMotion(); const [springs] = useSprings(NUM_EMOJIS, i => ({ ...to(i, onAnimationEnd), from: from(i), + immediate: reducedMotion, config: { mass: 20, tension: 120, diff --git a/ts/components/App.tsx b/ts/components/App.tsx index 9ab276a1e6..54053cebbd 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { useEffect } from 'react'; -import { Globals } from '@react-spring/web'; import classNames from 'classnames'; import type { ViewStoryActionCreatorType } from '../state/ducks/stories'; @@ -13,7 +12,6 @@ import { type AppStateType, AppViewType } from '../state/ducks/app'; import { SmartInstallScreen } from '../state/smart/InstallScreen'; import { StandaloneRegistration } from './StandaloneRegistration'; import { usePageVisibility } from '../hooks/usePageVisibility'; -import { useReducedMotion } from '../hooks/useReducedMotion'; type PropsType = { state: AppStateType; @@ -124,14 +122,6 @@ export function App({ document.body.classList.toggle('page-is-visible', isPageVisible); }, [isPageVisible]); - // A11y settings for react-spring - const prefersReducedMotion = useReducedMotion(); - useEffect(() => { - Globals.assign({ - skipAnimation: prefersReducedMotion, - }); - }, [prefersReducedMotion]); - return (
; @@ -124,7 +125,9 @@ export function AnimatedEmoji({ }: AnimatedEmojiProps): JSX.Element { const height = EMOJI_HEIGHT * toScale; + const reducedMotion = useReducedMotion(); const { rotate, x, y } = useSpring({ + immediate: reducedMotion, from: { rotate: fromRotate, x: fromX, @@ -142,6 +145,7 @@ export function AnimatedEmoji({ // These styles animate faster than Y. // Reactions toasts animate with opacity so harmonize with that. const { scale } = useSpring({ + immediate: reducedMotion, from: { scale: 0.5, }, diff --git a/ts/components/CallingRaisedHandsList.tsx b/ts/components/CallingRaisedHandsList.tsx index aa77e281cf..e4bf260fc6 100644 --- a/ts/components/CallingRaisedHandsList.tsx +++ b/ts/components/CallingRaisedHandsList.tsx @@ -14,6 +14,7 @@ import { ModalHost } from './ModalHost'; import { drop } from '../util/drop'; import * as log from '../logging/log'; import { usePrevious } from '../hooks/usePrevious'; +import { useReducedMotion } from '../hooks/useReducedMotion'; export type PropsType = { readonly i18n: LocalizerType; @@ -174,6 +175,8 @@ export function CallingRaisedHandsListButton({ }: CallingRaisedHandsListButtonPropsType): JSX.Element | null { const [isVisible, setIsVisible] = React.useState(raisedHandsCount > 0); + const reducedMotion = useReducedMotion(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME const [opacitySpringProps, opacitySpringApi] = useSpring( { @@ -186,6 +189,7 @@ export function CallingRaisedHandsListButton({ // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME const [scaleSpringProps, scaleSpringApi] = useSpring( { + immediate: reducedMotion, from: { scale: 0.9 }, to: { scale: 1 }, config: BUTTON_SCALE_SPRING_CONFIG, diff --git a/ts/components/CallingToast.tsx b/ts/components/CallingToast.tsx index 6de2e6f428..f4a086c35b 100644 --- a/ts/components/CallingToast.tsx +++ b/ts/components/CallingToast.tsx @@ -18,6 +18,7 @@ import { useIsMounted } from '../hooks/useIsMounted'; import type { LocalizerType } from '../types/I18N'; import { usePrevious } from '../hooks/usePrevious'; import { difference } from '../util/setUtil'; +import { useReducedMotion } from '../hooks/useReducedMotion'; const DEFAULT_LIFETIME = 5000; const DEFAULT_TRANSITION_FROM = { @@ -225,7 +226,10 @@ export function CallingToastProvider({ const toastsRemoved = difference(prevToasts, curToasts); const toastsAdded = difference(curToasts, prevToasts); + const reducedMotion = useReducedMotion(); + const transitions = useTransition(toasts, { + immediate: reducedMotion, from: item => { const enteringItemIndex = toasts.findIndex( toast => toast.key === item.key diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 84e528aa26..71ff71dc8d 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -30,6 +30,7 @@ import { drop } from '../util/drop'; import { isCmdOrCtrl } from '../hooks/useKeyboardShortcuts'; import type { ForwardMessagesPayload } from '../state/ducks/globalModals'; import { ForwardMessagesModalType } from './ForwardMessagesModal'; +import { useReducedMotion } from '../hooks/useReducedMotion'; export type PropsType = { children?: ReactNode; @@ -322,9 +323,12 @@ export function Lightbox({ const thumbnailsMarginInlineStart = 0 - (selectedIndex * THUMBNAIL_FULL_WIDTH + THUMBNAIL_WIDTH / 2); + const reducedMotion = useReducedMotion(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME const [thumbnailsStyle, thumbnailsAnimation] = useSpring( { + immediate: reducedMotion, config: THUMBNAIL_SPRING_CONFIG, to: { marginInlineStart: thumbnailsMarginInlineStart, diff --git a/ts/components/PlaybackButton.tsx b/ts/components/PlaybackButton.tsx index 89b9545a46..5884b0a698 100644 --- a/ts/components/PlaybackButton.tsx +++ b/ts/components/PlaybackButton.tsx @@ -4,6 +4,7 @@ import { animated, useSpring } from '@react-spring/web'; import classNames from 'classnames'; import React, { useCallback } from 'react'; +import { useReducedMotion } from '../hooks/useReducedMotion'; const SPRING_CONFIG = { mass: 0.5, @@ -27,9 +28,11 @@ export type ButtonProps = { export const PlaybackButton = React.forwardRef( function ButtonInner(props, ref) { const { mod, label, variant, onClick, context, visible = true } = props; + const reducedMotion = useReducedMotion(); // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME const [animProps] = useSpring( { + immediate: reducedMotion, config: SPRING_CONFIG, to: { scale: visible ? 1 : 0 }, }, diff --git a/ts/components/PlaybackRateButton.tsx b/ts/components/PlaybackRateButton.tsx index bb121bde7c..9a25a0bb62 100644 --- a/ts/components/PlaybackRateButton.tsx +++ b/ts/components/PlaybackRateButton.tsx @@ -5,6 +5,7 @@ import classNames from 'classnames'; import React, { useCallback, useState } from 'react'; import { animated, useSpring } from '@react-spring/web'; import type { LocalizerType } from '../types/Util'; +import { useReducedMotion } from '../hooks/useReducedMotion'; const SPRING_CONFIG = { mass: 0.5, @@ -30,10 +31,12 @@ export function PlaybackRateButton({ onClick, }: Props): JSX.Element { const [isDown, setIsDown] = useState(false); + const reducedMotion = useReducedMotion(); // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME const [animProps] = useSpring( { + immediate: reducedMotion, config: SPRING_CONFIG, to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 }, }, diff --git a/ts/components/ProfileEditor.tsx b/ts/components/ProfileEditor.tsx index f436968fe6..25ff7b3218 100644 --- a/ts/components/ProfileEditor.tsx +++ b/ts/components/ProfileEditor.tsx @@ -47,6 +47,7 @@ import { isWhitespace, trim } from '../util/whitespaceStringUtil'; import { UserText } from './UserText'; import { Tooltip, TooltipPlacement } from './Tooltip'; import { offsetDistanceModifier } from '../util/popperUtil'; +import { useReducedMotion } from '../hooks/useReducedMotion'; export enum EditState { None = 'None', @@ -824,8 +825,9 @@ function UsernameLinkTooltip({ children: React.ReactNode; i18n: LocalizerType; }) { + const reducedMotion = useReducedMotion(); const animatedStyles = useSpring({ - from: { opacity: 0, scale: 0.25 }, + from: { opacity: 0, scale: reducedMotion ? 1 : 0.25 }, to: { opacity: 1, scale: 1 }, config: { mass: 1, tension: 280, friction: 25 }, delay: 200, diff --git a/ts/components/StoryProgressSegment.tsx b/ts/components/StoryProgressSegment.tsx index e3730ac198..246871bc57 100644 --- a/ts/components/StoryProgressSegment.tsx +++ b/ts/components/StoryProgressSegment.tsx @@ -29,10 +29,6 @@ export const StoryProgressSegment = memo(function StoryProgressSegment({ const [progressBarStyle] = useSpring(() => { return { - // Override default value from `Globals` to ignore "Reduce Motion" setting. - // This animation is important for progressing through stories and is minor - // enough that it shouldn't cause issues for users with sensitivity to motion. - skipAnimation: false, immediate: index !== currentIndex, // Pause while we are waiting for a valid duration pause: !playing || !isValidDuration(duration), diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx index 4672f3fa0e..7844e782fd 100644 --- a/ts/components/conversation/TypingBubble.tsx +++ b/ts/components/conversation/TypingBubble.tsx @@ -13,6 +13,7 @@ import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType } from '../../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; import { drop } from '../../util/drop'; +import { useReducedMotion } from '../../hooks/useReducedMotion'; const MAX_AVATARS_COUNT = 3; @@ -87,9 +88,11 @@ function TypingBubbleAvatar({ i18n: LocalizerType; theme: ThemeType; }): ReactElement | null { + const reducedMotion = useReducedMotion(); // eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME const [springProps, springApi] = useSpring( { + immediate: reducedMotion, config: SPRING_CONFIG, from: shouldAnimate ? AVATAR_ANIMATION_PROPS[visible ? 'hidden' : 'visible'] diff --git a/ts/hooks/useAnimated.tsx b/ts/hooks/useAnimated.tsx index 2f56550189..ce8dde1a72 100644 --- a/ts/hooks/useAnimated.tsx +++ b/ts/hooks/useAnimated.tsx @@ -4,6 +4,7 @@ import { useState, useCallback } from 'react'; import type { SpringValues } from '@react-spring/web'; import { useChain, useSpring, useSpringRef } from '@react-spring/web'; +import { useReducedMotion } from './useReducedMotion'; export type ModalConfigType = { opacity: number; @@ -33,6 +34,7 @@ export function useAnimated( modalStyles: SpringValues; overlayStyles: SpringValues; } { + const reducedMotion = useReducedMotion(); const [state, setState] = useState(ModalState.Opening); const shouldShowModal = state === ModalState.Open || state === ModalState.Opening; @@ -41,6 +43,7 @@ export function useAnimated( const modalRef = useSpringRef(); const modalStyles = useSpring({ + immediate: reducedMotion, from: getFrom(shouldShowModal), to: getTo(shouldShowModal), onRest: () => {