From 8cfda4094612df4b02a59120aba9cd0d97d6e8a9 Mon Sep 17 00:00:00 2001 From: Leroy Korterink Date: Fri, 13 Oct 2023 16:22:35 +0200 Subject: [PATCH] #207 Allow any target in expose animation hooks --- packages/react-animation/src/index.ts | 1 - .../src/useAnimationRef/useAnimationRef.mdx | 39 ----- .../useAnimationRef.stories.tsx | 52 ------- .../src/useAnimationRef/useAnimationRef.ts | 10 -- .../useExposeAnimation/useExposeAnimation.mdx | 8 +- .../useExposeAnimation/useExposeAnimation.ts | 7 +- .../useExposedAnimation.mdx | 2 + .../useExposedAnimation.stories.tsx | 134 ++++++++++++++---- .../useExposedAnimation.ts | 9 +- .../useExposedAnimations.ts | 14 +- 10 files changed, 135 insertions(+), 141 deletions(-) delete mode 100644 packages/react-animation/src/useAnimationRef/useAnimationRef.mdx delete mode 100644 packages/react-animation/src/useAnimationRef/useAnimationRef.stories.tsx delete mode 100644 packages/react-animation/src/useAnimationRef/useAnimationRef.ts diff --git a/packages/react-animation/src/index.ts b/packages/react-animation/src/index.ts index b96fea7..9a3e5e2 100644 --- a/packages/react-animation/src/index.ts +++ b/packages/react-animation/src/index.ts @@ -1,7 +1,6 @@ export * from './SplitTextWrapper/SplitTextWrapper.js'; export * from './getAnimation/getAnimation.js'; export * from './useAnimation/useAnimation.js'; -export * from './useAnimationRef/useAnimationRef.js'; export * from './useExposeAnimation/useExposeAnimation.js'; export * from './useExposedAnimation/useExposedAnimation.js'; export * from './useExposedAnimations/useExposedAnimations.js'; diff --git a/packages/react-animation/src/useAnimationRef/useAnimationRef.mdx b/packages/react-animation/src/useAnimationRef/useAnimationRef.mdx deleted file mode 100644 index 25d6af1..0000000 --- a/packages/react-animation/src/useAnimationRef/useAnimationRef.mdx +++ /dev/null @@ -1,39 +0,0 @@ -import { Meta } from '@storybook/blocks'; - - - -# useAnimationRef - -The `useAnimationRef` hook is used to handle optional refs for exposed animations. - -```tsx -type ChildProps = { - animationRef?: MutableRefObject; -} - -function Child({ animationRef }: ChildProps): ReactElement { - const animation = useAnimation(() => gsap.from({ value: 0 }, { value: 1 }), []); - - useExposeAnimation( - animation, - // Use `useAnimationRef` to handle optional refs - useAnimationRef(animationRef) - ); - - ... -} - -function Parent(): ReactElement { - const myRef = useRef(symbol()); - - return ( - - ); -} -``` - -## Signature - -```tsx -function useAnimationRef(ref?: MutableRefObject): RefObject; -``` diff --git a/packages/react-animation/src/useAnimationRef/useAnimationRef.stories.tsx b/packages/react-animation/src/useAnimationRef/useAnimationRef.stories.tsx deleted file mode 100644 index 9774543..0000000 --- a/packages/react-animation/src/useAnimationRef/useAnimationRef.stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { type Meta, type StoryObj } from '@storybook/react'; -import gsap from 'gsap'; -import { useEffect, type ReactElement, type RefObject } from 'react'; -import { getAnimation } from '../index.js'; -import { useAnimation } from '../useAnimation/useAnimation.js'; -import { useExposeAnimation } from '../useExposeAnimation/useExposeAnimation.js'; -import { useAnimationRef } from './useAnimationRef.js'; - -const meta = { - title: 'hooks/useAnimationRef', -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -type ChildProps = { - animationRef?: RefObject; -}; - -function Child({ animationRef }: ChildProps): ReactElement { - const animation = useAnimation(() => gsap.from({ value: 0 }, { value: 1 }), []); - - useExposeAnimation( - animation, - // Also use animation ref when exposing animation to deal with optional props - useAnimationRef(animationRef), - ); - - return
; -} - -export const UseAnimationRef = { - render(): ReactElement { - const animationRef = useAnimationRef(); - - useEffect(() => { - // eslint-disable-next-line no-console - console.log('Mount\n', animationRef.current, getAnimation(animationRef.current)); - }, [animationRef]); - - // eslint-disable-next-line no-console - console.log('Render\n', animationRef.current, getAnimation(animationRef.current)); - - return ( - <> -
Check the console to see the result
- - - - ); - }, -} satisfies Story; diff --git a/packages/react-animation/src/useAnimationRef/useAnimationRef.ts b/packages/react-animation/src/useAnimationRef/useAnimationRef.ts deleted file mode 100644 index 9a17697..0000000 --- a/packages/react-animation/src/useAnimationRef/useAnimationRef.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useRef, type RefObject } from 'react'; - -export function useAnimationRef(ref?: RefObject): RefObject { - const fallbackRef = useRef(Symbol('useAnimationRef')); - const animationRef = useRef(fallbackRef.current); - - animationRef.current = ref?.current ?? fallbackRef.current; - - return animationRef; -} diff --git a/packages/react-animation/src/useExposeAnimation/useExposeAnimation.mdx b/packages/react-animation/src/useExposeAnimation/useExposeAnimation.mdx index 0dca0d9..61f2665 100644 --- a/packages/react-animation/src/useExposeAnimation/useExposeAnimation.mdx +++ b/packages/react-animation/src/useExposeAnimation/useExposeAnimation.mdx @@ -44,17 +44,19 @@ const UseExposeAnimationForwardRef = ensuredForwardRef( ); ``` -## Using a string value as reference +## Using a value as reference ```tsx +const value = Symbol("myRef") + const UseExposeAnimationAnyValue = (): ReactElement => { - const ref = useRef('myRef'); + const ref = useRef(value); const animation = useAnimation(() => gsap.from({ value: 0 }, { value: 1 }), []); useExposeAnimation(animation, ref); useEffect(() => { - console.log(getAnimation('myRef')); + console.log(getAnimation(value)); }, []); ... diff --git a/packages/react-animation/src/useExposeAnimation/useExposeAnimation.ts b/packages/react-animation/src/useExposeAnimation/useExposeAnimation.ts index 89577d2..1932682 100644 --- a/packages/react-animation/src/useExposeAnimation/useExposeAnimation.ts +++ b/packages/react-animation/src/useExposeAnimation/useExposeAnimation.ts @@ -1,3 +1,4 @@ +import { unref, type Unreffable } from '@mediamonks/react-hooks'; import { useEffect, type RefObject } from 'react'; import { animations } from '../animations.js'; @@ -6,15 +7,15 @@ import { animations } from '../animations.js'; */ export function useExposeAnimation( animation: RefObject, - reference: RefObject, + reference: Unreffable, ): void { useEffect(() => { // eslint-disable-next-line no-underscore-dangle - const _reference = reference.current; + const _reference = unref(reference); // eslint-disable-next-line no-underscore-dangle const _animation = animation.current; - if (_animation) { + if (_animation && _reference) { animations.set(_reference, _animation); } diff --git a/packages/react-animation/src/useExposedAnimation/useExposedAnimation.mdx b/packages/react-animation/src/useExposedAnimation/useExposedAnimation.mdx index 155fcba..2137126 100644 --- a/packages/react-animation/src/useExposedAnimation/useExposedAnimation.mdx +++ b/packages/react-animation/src/useExposedAnimation/useExposedAnimation.mdx @@ -127,3 +127,5 @@ function Parent(): ReactElement { return ; } ``` + + diff --git a/packages/react-animation/src/useExposedAnimation/useExposedAnimation.stories.tsx b/packages/react-animation/src/useExposedAnimation/useExposedAnimation.stories.tsx index 335f5ec..bee3bfb 100644 --- a/packages/react-animation/src/useExposedAnimation/useExposedAnimation.stories.tsx +++ b/packages/react-animation/src/useExposedAnimation/useExposedAnimation.stories.tsx @@ -1,22 +1,29 @@ -import { ensuredForwardRef, useEventListener } from '@mediamonks/react-hooks'; +import { ensuredForwardRef, useEventListener, useStaticValue } from '@mediamonks/react-hooks'; +import type { Meta, StoryObj, StoryFn } from '@storybook/react'; import gsap from 'gsap'; -import { useRef, type ReactElement, type RefObject } from 'react'; +import { useRef, type ReactElement } from 'react'; import { useAnimation } from '../useAnimation/useAnimation.js'; -import { useAnimationRef } from '../useAnimationRef/useAnimationRef.js'; import { useExposeAnimation } from '../useExposeAnimation/useExposeAnimation.js'; import { useExposedAnimation } from './useExposedAnimation.js'; -export default { +const meta = { title: 'hooks/useExposedAnimation', -}; +} satisfies Meta; + +export default meta; + +type Story = StoryObj; type ChildProps = { - rotateAnimationRef?: RefObject; - scaleAnimationRef?: RefObject; + rotateAnimationReference?: symbol; + scaleAnimationReference?: symbol; }; const Child = ensuredForwardRef( - ({ rotateAnimationRef, scaleAnimationRef }, ref): ReactElement => { + ( + { rotateAnimationReference: rotateAnimationRef, scaleAnimationReference: scaleAnimationRef }, + ref, + ): ReactElement => { const animation = useAnimation( () => gsap.from( @@ -31,7 +38,27 @@ const Child = ensuredForwardRef( ); const rotateAnimation = useAnimation( - () => gsap.timeline({ paused: true }).fromTo(ref.current, { rotate: 0 }, { rotate: 360 }), + () => + gsap + .timeline({ paused: true }) + .to(ref.current, { + x: 20, + y: 20, + duration: 0.4, + ease: 'power2.inOut', + }) + .to(ref.current, { + x: -20, + y: -20, + duration: 0.4, + ease: 'power2.out', + }) + .to(ref.current, { + x: 0, + y: 0, + duration: 0.6, + ease: 'power2.out', + }), [], ); @@ -39,24 +66,29 @@ const Child = ensuredForwardRef( () => gsap .timeline({ paused: true }) - .fromTo(ref.current, { scale: 1 }, { scale: 0.5 }) - .to(ref.current, { scale: 1 }), + .fromTo(ref.current, { scale: 1 }, { scale: 0.75, ease: 'power2.out' }) + .to(ref.current, { scale: 1, ease: 'power2.out' }), [], ); useExposeAnimation(animation, ref); - useExposeAnimation(rotateAnimation, useAnimationRef(rotateAnimationRef)); - useExposeAnimation(scaleAnimation, useAnimationRef(scaleAnimationRef)); + useExposeAnimation(rotateAnimation, rotateAnimationRef); + useExposeAnimation(scaleAnimation, scaleAnimationRef); return ( -
- Check the console to see the result -
+
); }, ); -export const UseExposedAnimation = { +export const UseExposedAnimation: Story = { render(): ReactElement { const ref = useRef(null); const animation = useExposedAnimation(ref); @@ -69,14 +101,22 @@ export const UseExposedAnimation = { }; export const MultipleChildComponentAnimations = { + decorators: [ + // eslint-disable-next-line @typescript-eslint/naming-convention + (Story: StoryFn): ReactElement => ( +
+ +
+ ), + ], render(): ReactElement { const ref = useRef(null); - const rotateAnimationRef = useAnimationRef(); - const rotateAnimation = useExposedAnimation(rotateAnimationRef); + const rotateAnimationReference = useStaticValue(() => Symbol('rotateAnimation')); + const scaleAnimationReference = useStaticValue(() => Symbol('scaleAnimation')); - const scaleAnimationRef = useAnimationRef(); - const scaleAnimation = useExposedAnimation(scaleAnimationRef); + const rotateAnimation = useExposedAnimation(rotateAnimationReference); + const scaleAnimation = useExposedAnimation(scaleAnimationReference); useEventListener(globalThis.document, 'keydown', (event) => { if (event instanceof KeyboardEvent) { @@ -105,10 +145,56 @@ export const MultipleChildComponentAnimations = { <> -

Press `z` to rotate, press `x` to scale

+

+ Press `z` to wiggle, press `x` to scale +

+ + ); + }, +}; + +export const OptionalMultipleChildComponentAnimations: Story = { + decorators: [ + // eslint-disable-next-line @typescript-eslint/naming-convention + (Story: StoryFn): ReactElement => ( +
+ +
+ ), + ], + render(): ReactElement { + const ref = useRef(null); + const rotateAnimationReference = useStaticValue(() => Symbol('rotateAnimation')); + const rotateAnimation = useExposedAnimation(rotateAnimationReference); + + useEventListener(globalThis.document, 'keydown', (event) => { + if (event instanceof KeyboardEvent) { + switch (event.key) { + case 'z': { + rotateAnimation?.play(0); + break; + } + default: { + break; + } + } + } + }); + + const animation = useExposedAnimation(ref); + + // eslint-disable-next-line no-console + console.log('useExposedAnimation', animation); + + return ( + <> + +

+ Press `z` to wiggle, the scale animation is not enabled +

); }, diff --git a/packages/react-animation/src/useExposedAnimation/useExposedAnimation.ts b/packages/react-animation/src/useExposedAnimation/useExposedAnimation.ts index ac7e16c..69e853f 100644 --- a/packages/react-animation/src/useExposedAnimation/useExposedAnimation.ts +++ b/packages/react-animation/src/useExposedAnimation/useExposedAnimation.ts @@ -1,20 +1,21 @@ -import { useEffect, useState, type RefObject } from 'react'; +import { unref, type Unreffable } from '@mediamonks/react-hooks'; +import { useEffect, useState } from 'react'; import { animations } from '../animations.js'; /** * Hook to get animation from global animations map using given reference */ export function useExposedAnimation( - ref: RefObject, + target: Unreffable, ): T | undefined { const [animation, setAnimation] = useState(); useEffect( () => animations.listen(() => { - setAnimation(animations.get(ref.current)); + setAnimation(animations.get(unref(target))); }), - [ref], + [target], ); return animation; diff --git a/packages/react-animation/src/useExposedAnimations/useExposedAnimations.ts b/packages/react-animation/src/useExposedAnimations/useExposedAnimations.ts index 66f6829..41763eb 100644 --- a/packages/react-animation/src/useExposedAnimations/useExposedAnimations.ts +++ b/packages/react-animation/src/useExposedAnimations/useExposedAnimations.ts @@ -1,11 +1,12 @@ -import { useEffect, useState, type RefObject } from 'react'; +import { unref, type Unreffable } from '@mediamonks/react-hooks'; +import { useEffect, useState } from 'react'; import { animations } from '../animations.js'; /** * Hook to get animation from global animations map using given reference */ export function useExposedAnimations( - arrayRef: RefObject>, + target: Unreffable>, ): ReadonlyArray { const [exposedAnimations, setExposedAnimations] = useState>( [], @@ -14,8 +15,10 @@ export function useExposedAnimations( useEffect( () => animations.listen(() => { - if (arrayRef.current) { - const newAnimations = arrayRef.current.map((ref) => animations.get(ref)); + const array = unref(target); + + if (array) { + const newAnimations = array.map((ref) => animations.get(ref)); // this should only be done when the refs have been updated, otherwise we're returning // a new array ref with the same values, which will cause a re-render setExposedAnimations((currentAnimations) => @@ -23,7 +26,8 @@ export function useExposedAnimations( ); } }), - [arrayRef], + // eslint-disable-next-line react-hooks/exhaustive-deps + [target], ); return exposedAnimations;