Skip to content

Commit

Permalink
fix(useReducedMotion): use state (#8008)
Browse files Browse the repository at this point in the history
  • Loading branch information
SevereCloud authored Nov 28, 2024
1 parent b82c391 commit a698551
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 61 deletions.
58 changes: 10 additions & 48 deletions packages/vkui/src/components/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
'use client';

import * as React from 'react';
import { Icon16Spinner, Icon24Spinner, Icon32Spinner, Icon44Spinner } from '@vkontakte/icons';
import { classNames, hasReactNode } from '@vkontakte/vkjs';
import { useReducedMotion } from '../../lib/animation';
import type { HTMLAttributesWithRootRef } from '../../types';
import { RootComponent } from '../RootComponent/RootComponent';
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden';
import { SpinnerAnimation } from './SpinnerAnimation';
import styles from './Spinner.module.css';

const spinnerIconMap = {
s: Icon16Spinner,
m: Icon24Spinner,
l: Icon32Spinner,
xl: Icon44Spinner,
};

export interface SpinnerProps extends HTMLAttributesWithRootRef<HTMLSpanElement> {
size?: 's' | 'm' | 'l' | 'xl';
disableAnimation?: boolean;
Expand All @@ -29,50 +34,7 @@ export const Spinner: React.FC<SpinnerProps> = React.memo(
noColor = false,
...restProps
}: SpinnerProps) => {
const isReducedMotion = useReducedMotion();
const SpinnerIcon = {
s: Icon16Spinner,
m: Icon24Spinner,
l: Icon32Spinner,
xl: Icon44Spinner,
}[size];
let svgAnimateElement: React.ReactNode = null;

const [isReadyForSetSVGAnimateElement, setIsReadyForSetSVGAnimateElement] = React.useState(
disableAnimation ? true : false,
);

React.useEffect(function waitReactHydrationBeforeSetSVGAnimateElement() {
setIsReadyForSetSVGAnimateElement(true);
}, []);

if (isReadyForSetSVGAnimateElement && !disableAnimation) {
if (isReducedMotion) {
svgAnimateElement = (
<animate
attributeName="opacity"
keyTimes="0; 0.5; 1"
values="1; 0.1; 1"
begin="0s"
dur="2s"
repeatCount="indefinite"
/>
);
} else {
const center = { s: 8, m: 12, l: 16, xl: 22 }[size];
svgAnimateElement = (
<animateTransform
attributeType="XML"
attributeName="transform"
type="rotate"
from={`0 ${center} ${center}`}
to={`360 ${center} ${center}`}
dur="0.7s"
repeatCount="indefinite"
/>
);
}
}
const SpinnerIcon = spinnerIconMap[size];

return (
<RootComponent
Expand All @@ -81,7 +43,7 @@ export const Spinner: React.FC<SpinnerProps> = React.memo(
{...restProps}
baseClassName={classNames(styles.host, noColor && styles.noColor)}
>
<SpinnerIcon>{svgAnimateElement}</SpinnerIcon>
<SpinnerIcon>{disableAnimation ? null : <SpinnerAnimation size={size} />}</SpinnerIcon>
{hasReactNode(children) && <VisuallyHidden>{children}</VisuallyHidden>}
</RootComponent>
);
Expand Down
42 changes: 42 additions & 0 deletions packages/vkui/src/components/Spinner/SpinnerAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { useReducedMotion } from '../../lib/animation';
import { type SpinnerProps } from './Spinner';

interface SpinnerAnimationProps {
size: SpinnerProps['size'];
}

export function SpinnerAnimation({ size = 'm' }: SpinnerAnimationProps) {
const isReducedMotion = useReducedMotion();

if (isReducedMotion === undefined) {
return null;
}

if (isReducedMotion) {
return (
<animate
attributeName="opacity"
keyTimes="0; 0.5; 1"
values="1; 0.1; 1"
begin="0s"
dur="2s"
repeatCount="indefinite"
/>
);
}

const center = { s: 8, m: 12, l: 16, xl: 22 }[size];
return (
<animateTransform
attributeType="XML"
attributeName="transform"
type="rotate"
from={`0 ${center} ${center}`}
to={`360 ${center} ${center}`}
dur="0.7s"
repeatCount="indefinite"
/>
);
}
19 changes: 6 additions & 13 deletions packages/vkui/src/lib/animation/useReducedMotion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,26 @@ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';

export const REDUCE_MOTION_MEDIA_QUERY = 'screen and (prefers-reduced-motion: reduce)';

export const useReducedMotion = (): boolean => {
export const useReducedMotion = (): boolean | undefined => {
const { window } = useDOM();
const initial = React.useMemo(
() =>
window
? window.matchMedia(REDUCE_MOTION_MEDIA_QUERY).matches
: /* istanbul ignore next: на текущий момент, покрытие данного кейса неинтересно */
false,
[window],
);
const reducedMotion = React.useRef(initial);

const [reducedMotion, setReducedMotion] = React.useState<boolean | undefined>(() => undefined);

useIsomorphicLayoutEffect(() => {
/* istanbul ignore if: невозможный кейс (в SSR вызова этой функции не будет) */
if (!window) {
return;
}
const match = window.matchMedia(REDUCE_MOTION_MEDIA_QUERY);
reducedMotion.current = match.matches;
setReducedMotion(match.matches);
/* istanbul ignore next: на текущий момент, покрытие данного кейса неинтересно */
const handleMediaQueryChange = (event: MediaQueryListEvent) => {
/* istanbul ignore next */
reducedMotion.current = event.matches;
setReducedMotion(event.matches);
};
matchMediaListAddListener(match, handleMediaQueryChange);
return () => matchMediaListRemoveListener(match, handleMediaQueryChange);
}, [window]);

return reducedMotion.current;
return reducedMotion;
};

0 comments on commit a698551

Please sign in to comment.