From 757201fdc5c04a3f15504f74bf8ab85bb6018c2b Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 27 Oct 2023 11:09:38 +0200 Subject: refactor(hooks,provider): move reduce motion setter Since the local storage key is not meant to change between the components, it should be set directly inside the app file. So both the local storage and the data attribute should be handle in a provider. I also added a custom document because we need a script to retrieve the stored value in local storage earlier to avoid flashing on hydration. --- src/utils/providers/index.ts | 1 + src/utils/providers/motion-provider/index.ts | 1 + .../motion-provider/motion-provider.test.tsx | 50 +++++++++++++ .../providers/motion-provider/motion-provider.tsx | 83 ++++++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 src/utils/providers/motion-provider/index.ts create mode 100644 src/utils/providers/motion-provider/motion-provider.test.tsx create mode 100644 src/utils/providers/motion-provider/motion-provider.tsx (limited to 'src/utils/providers') diff --git a/src/utils/providers/index.ts b/src/utils/providers/index.ts index 640730f..a01200a 100644 --- a/src/utils/providers/index.ts +++ b/src/utils/providers/index.ts @@ -1,2 +1,3 @@ export * from './ackee-provider'; +export * from './motion-provider'; export * from './prism-theme'; diff --git a/src/utils/providers/motion-provider/index.ts b/src/utils/providers/motion-provider/index.ts new file mode 100644 index 0000000..67f493e --- /dev/null +++ b/src/utils/providers/motion-provider/index.ts @@ -0,0 +1 @@ +export * from './motion-provider'; diff --git a/src/utils/providers/motion-provider/motion-provider.test.tsx b/src/utils/providers/motion-provider/motion-provider.test.tsx new file mode 100644 index 0000000..3a02e6f --- /dev/null +++ b/src/utils/providers/motion-provider/motion-provider.test.tsx @@ -0,0 +1,50 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { type FC, useContext } from 'react'; +import { MotionContext, MotionProvider } from './motion-provider'; + +const bodyPrefix = 'Motion is reduced:'; + +const ComponentTest: FC = () => { + const { isReduced } = useContext(MotionContext); + + return ( +
+ {bodyPrefix} {`${isReduced}`} +
+ ); +}; + +describe('MotionProvider', () => { + it('uses the default value when the provider is not used', () => { + const defaultValue = false; + + render(); + + expect(rtlScreen.getByText(new RegExp(bodyPrefix))).toHaveTextContent( + `${bodyPrefix} ${defaultValue}` + ); + }); + + it('provides the given value to its children and set a matching attribute', () => { + const attribute = 'eius'; + const isReduced = true; + + const { baseElement } = render( + + + + ); + + expect(rtlScreen.getByText(new RegExp(bodyPrefix))).toHaveTextContent( + `${bodyPrefix} ${isReduced}` + ); + expect(baseElement.parentElement?.getAttribute(`data-${attribute}`)).toBe( + `${isReduced}` + ); + }); +}); diff --git a/src/utils/providers/motion-provider/motion-provider.tsx b/src/utils/providers/motion-provider/motion-provider.tsx new file mode 100644 index 0000000..dfedcaa --- /dev/null +++ b/src/utils/providers/motion-provider/motion-provider.tsx @@ -0,0 +1,83 @@ +import { + type Dispatch, + type FC, + type ReactNode, + type SetStateAction, + createContext, + useMemo, + useCallback, + useEffect, +} from 'react'; +import { getDataAttributeFrom } from '../../helpers'; +import { useLocalStorage } from '../../hooks'; + +type MotionContextProps = { + isReduced: boolean; + setIsReduced: Dispatch>; + toggleReducedMotion: () => void; +}; + +export const MotionContext = createContext({ + isReduced: false, + setIsReduced: (value) => value, + toggleReducedMotion: () => null, +}); + +const validator = (value: unknown): value is boolean => + typeof value === 'boolean'; + +export type MotionProviderProps = { + /** + * The attribute name to append to document root. + */ + attribute: string; + /** + * The provider children. + */ + children?: ReactNode; + /** + * Is reduced motion currently active? + * + * @default false + */ + hasReducedMotion?: boolean; + /** + * The key to use in local storage. + */ + storageKey: string; +}; + +export const MotionProvider: FC = ({ + attribute, + children, + hasReducedMotion = false, + storageKey, +}) => { + const [isReduced, setIsReduced] = useLocalStorage( + storageKey, + hasReducedMotion, + validator + ); + const dataAttribute = getDataAttributeFrom(attribute); + + useEffect(() => { + if (typeof window !== 'undefined') + document.documentElement.setAttribute(dataAttribute, `${isReduced}`); + + return () => { + document.documentElement.removeAttribute(dataAttribute); + }; + }, [dataAttribute, isReduced]); + + const toggleReducedMotion = useCallback(() => { + setIsReduced((prevState) => !prevState); + }, [setIsReduced]); + + const value: MotionContextProps = useMemo(() => { + return { isReduced, setIsReduced, toggleReducedMotion }; + }, [isReduced, setIsReduced, toggleReducedMotion]); + + return ( + {children} + ); +}; -- cgit v1.2.3