From 639871a36ef5b8a80c1a746ddebc84fc19a03bb6 Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Tue, 12 Nov 2024 10:43:07 -0500 Subject: [PATCH 1/2] feat(motion): introduce opacity atom and refactor collapse and fade components --- .../library/src/components/Atoms/Atoms.ts | 21 ++++++++++ .../src/components/Atoms/Atoms.types.ts | 7 ++++ .../library/src/components/Atoms/index.ts | 1 + .../src/components/Collapse/Collapse.ts | 15 +++----- .../src/components/Collapse/collapse-atoms.ts | 38 ------------------- .../library/src/components/Fade/Fade.ts | 20 ++-------- .../library/src/components/Fade/Fade.types.ts | 13 +++++++ .../src/components/Scale/Scale.types.ts | 20 ++++++++++ 8 files changed, 71 insertions(+), 64 deletions(-) create mode 100644 packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts create mode 100644 packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.types.ts create mode 100644 packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts create mode 100644 packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.types.ts create mode 100644 packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts new file mode 100644 index 00000000000000..5562f05f41c344 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts @@ -0,0 +1,21 @@ +import { AtomMotion } from '@fluentui/react-motion'; +import type { MotionAtomProps } from './Atoms.types'; + +export const motionAtom = ({ + keyframes, + fill = 'both', + ...props +}: MotionAtomProps & { keyframes: Keyframe[] }): AtomMotion => ({ + keyframes, + ...props, +}); + +export const opacityAtom = ({ + fromOpacity = 0, + toOpacity = 1, + ...props +}: { fromOpacity?: number; toOpacity?: number } & MotionAtomProps): AtomMotion => + motionAtom({ + keyframes: [{ opacity: fromOpacity }, { opacity: toOpacity }], + ...props, + }); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.types.ts b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.types.ts new file mode 100644 index 00000000000000..238d1f432bfa8d --- /dev/null +++ b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.types.ts @@ -0,0 +1,7 @@ +export type MotionAtomProps = { + duration: number; + easing: string; + delay?: number; + direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'; + fill?: 'none' | 'forwards' | 'backwards' | 'both'; +}; diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts new file mode 100644 index 00000000000000..9830d54a0273dd --- /dev/null +++ b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts @@ -0,0 +1 @@ +export { opacityAtom } from './Atoms'; diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts index 6aa4b7bb66d5dc..b201db40df0881 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.ts @@ -1,14 +1,8 @@ import { motionTokens, createPresenceComponent, AtomMotion } from '@fluentui/react-motion'; import type { PresenceMotionFnCreator } from '../../types'; import type { CollapseDelayedVariantParams, CollapseRuntimeParams, CollapseVariantParams } from './collapse-types'; -import { - sizeEnterAtom, - whitespaceEnterAtom, - opacityEnterAtom, - opacityExitAtom, - sizeExitAtom, - whitespaceExitAtom, -} from './collapse-atoms'; +import { sizeEnterAtom, whitespaceEnterAtom, sizeExitAtom, whitespaceExitAtom } from './collapse-atoms'; +import { opacityAtom } from '../Atoms'; /** Define a presence motion for collapse/expand that can stagger the size and opacity motions by a given delay. */ export const createCollapseDelayedPresence: PresenceMotionFnCreator< @@ -47,7 +41,7 @@ export const createCollapseDelayedPresence: PresenceMotionFnCreator< // Fade in only if animateOpacity is true. Otherwise, leave opacity unaffected. if (animateOpacity) { enterAtoms.push( - opacityEnterAtom({ + opacityAtom({ duration: enterOpacityDuration, easing: enterEasing, delay: enterDelay, @@ -61,9 +55,10 @@ export const createCollapseDelayedPresence: PresenceMotionFnCreator< // Fade out only if animateOpacity is true. Otherwise, leave opacity unaffected. if (animateOpacity) { exitAtoms.push( - opacityExitAtom({ + opacityAtom({ duration: exitOpacityDuration, easing: exitEasing, + direction: 'reverse', }), ); } diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/collapse-atoms.ts b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/collapse-atoms.ts index 777223f5ddd356..8c239b61063b56 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/collapse-atoms.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/collapse-atoms.ts @@ -115,41 +115,3 @@ export const whitespaceExitAtom = ({ delay, }; }; - -// ----- OPACITY ----- - -export const opacityEnterAtom = ({ - duration, - easing, - delay = 0, - fromOpacity = 0, - toOpacity = 1, -}: { - duration: number; - easing: string; - delay?: number; - fromOpacity?: number; - toOpacity?: number; -}): AtomMotion => ({ - keyframes: [{ opacity: fromOpacity }, { opacity: toOpacity }], - duration, - easing, - delay, - fill: 'both', -}); - -export const opacityExitAtom = ({ - duration, - easing, - fromOpacity = 0, - toOpacity = 1, -}: { - duration: number; - easing: string; - fromOpacity?: number; - toOpacity?: number; -}): AtomMotion => ({ - keyframes: [{ opacity: toOpacity }, { opacity: fromOpacity }], - duration, - easing, -}); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.ts b/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.ts index 03ca3ea60567a1..3bef5bdc4cbe58 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.ts @@ -1,19 +1,7 @@ import { motionTokens, createPresenceComponent } from '@fluentui/react-motion'; import type { PresenceMotionCreator } from '../../types'; - -type FadeVariantParams = { - /** Time (ms) for the enter transition (fade-in). Defaults to the `durationNormal` value (200 ms). */ - enterDuration?: number; - - /** Easing curve for the enter transition (fade-in). Defaults to the `easeEase` value. */ - enterEasing?: string; - - /** Time (ms) for the exit transition (fade-out). Defaults to the `enterDuration` param for symmetry. */ - exitDuration?: number; - - /** Easing curve for the exit transition (fade-out). Defaults to the `enterEasing` param for symmetry. */ - exitEasing?: string; -}; +import type { FadeVariantParams } from './Fade.types'; +import { opacityAtom } from '../Atoms'; /** Define a presence motion for fade in/out */ export const createFadePresence: PresenceMotionCreator = ({ @@ -22,8 +10,8 @@ export const createFadePresence: PresenceMotionCreator = ({ exitDuration = enterDuration, exitEasing = enterEasing, } = {}) => ({ - enter: { duration: enterDuration, easing: enterEasing, keyframes: [{ opacity: 0 }, { opacity: 1 }] }, - exit: { duration: exitDuration, easing: exitEasing, keyframes: [{ opacity: 1 }, { opacity: 0 }] }, + enter: opacityAtom({ duration: enterDuration, easing: enterEasing }), + exit: opacityAtom({ duration: exitDuration, easing: exitEasing, direction: 'reverse' }), }); /** A React component that applies fade in/out transitions to its children. */ diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.types.ts b/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.types.ts new file mode 100644 index 00000000000000..35574730a91d74 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.types.ts @@ -0,0 +1,13 @@ +export type FadeVariantParams = { + /** Time (ms) for the enter transition (fade-in). Defaults to the `durationNormal` value (200 ms). */ + enterDuration?: number; + + /** Easing curve for the enter transition (fade-in). Defaults to the `easeEase` value. */ + enterEasing?: string; + + /** Time (ms) for the exit transition (fade-out). Defaults to the `enterDuration` param for symmetry. */ + exitDuration?: number; + + /** Easing curve for the exit transition (fade-out). Defaults to the `enterEasing` param for symmetry. */ + exitEasing?: string; +}; diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts new file mode 100644 index 00000000000000..ed14a11200efe7 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts @@ -0,0 +1,20 @@ +// eslint-disable-next-line @typescript-eslint/naming-convention +export type ScaleRuntimeParams_unstable = { + /** Whether to animate the opacity. Defaults to `true`. */ + animateOpacity?: boolean; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export type ScaleVariantParams_unstable = { + /** Time (ms) for the enter transition (expand). Defaults to the `durationNormal` value (200 ms). */ + enterScaleDuration?: number; + + /** Easing curve for the enter transition (expand). Defaults to the `easeEaseMax` value. */ + enterScaleEasing?: string; + + /** Time (ms) for the exit transition (collapse). Defaults to the `enterDuration` param for symmetry. */ + exitScaleDuration?: number; + + /** Easing curve for the exit transition (collapse). Defaults to the `enterEasing` param for symmetry. */ + exitScaleEasing?: string; +}; From 75ec58b3c9440731ec00f6188283f95d95982b8a Mon Sep 17 00:00:00 2001 From: Alexander Katrukhin Date: Tue, 12 Nov 2024 11:25:33 -0500 Subject: [PATCH 2/2] feat(scale): add scaleAtom and refactor Scale component for enhanced animations --- .../library/src/components/Atoms/Atoms.ts | 13 +++ .../library/src/components/Atoms/index.ts | 2 +- .../src/components/Collapse/Collapse.test.ts | 38 ++++++- .../library/src/components/Fade/Fade.test.tsx | 8 +- .../src/components/Scale/Scale.test.ts | 25 ++++- .../library/src/components/Scale/Scale.ts | 100 +++++++++++------- .../src/components/Scale/Scale.types.ts | 28 ++--- .../library/src/testing/testUtils.ts | 2 +- 8 files changed, 156 insertions(+), 60 deletions(-) diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts index 5562f05f41c344..9338d3c2057755 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/Atoms.ts @@ -19,3 +19,16 @@ export const opacityAtom = ({ keyframes: [{ opacity: fromOpacity }, { opacity: toOpacity }], ...props, }); + +export const scaleAtom = ({ + fromScale = 0.9, + toScale = 1, + ...props +}: { fromScale?: number; toScale?: number } & MotionAtomProps): AtomMotion => + motionAtom({ + keyframes: [ + { transform: `scale3d(${fromScale}, ${fromScale}, 1)`, visibility: 'visible' }, + { transform: `scale3d(${toScale}, ${toScale}, 1)` }, + ], + ...props, + }); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts index 9830d54a0273dd..17031f4153027e 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Atoms/index.ts @@ -1 +1 @@ -export { opacityAtom } from './Atoms'; +export { opacityAtom, scaleAtom } from './Atoms'; diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.test.ts b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.test.ts index e39bed15348e5b..7939b083f2acaa 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.test.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Collapse/Collapse.test.ts @@ -1,6 +1,42 @@ -import { expectPresenceMotionFunction, expectPresenceMotionArray } from '../../testing/testUtils'; +import { motionTokens, PresenceComponent } from '@fluentui/react-motion'; +import { expectPresenceMotionFunction, getMotionFunction } from '../../testing/testUtils'; import { Collapse } from './Collapse'; +function expectPresenceMotionArray(component: PresenceComponent) { + const presenceMotionFn = getMotionFunction(component); + + // eslint-disable-next-line @nx/workspace-no-restricted-globals + expect(presenceMotionFn?.({ element: document.createElement('div') })).toMatchObject({ + enter: expect.arrayContaining([ + expect.objectContaining({ + duration: expect.any(Number), + easing: expect.any(String), + keyframes: expect.any(Array), + }), + ]), + exit: expect.arrayContaining([ + expect.objectContaining({ + duration: expect.any(Number), + easing: expect.any(String), + keyframes: expect.any(Array), + }), + expect.objectContaining({ + duration: motionTokens.durationNormal, + easing: motionTokens.curveEasyEaseMax, + keyframes: expect.arrayContaining([ + expect.objectContaining({ maxHeight: '0px' }), + expect.objectContaining({ maxHeight: '0' }), + ]), + }), + expect.objectContaining({ + duration: motionTokens.durationNormal, + easing: motionTokens.curveEasyEaseMax, + keyframes: expect.arrayContaining([expect.objectContaining({ paddingBottom: '0', paddingTop: '0' })]), + }), + ]), + }); +} + describe('Collapse', () => { it('stores its motion definition as a static function', () => { expectPresenceMotionFunction(Collapse); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.test.tsx b/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.test.tsx index 5da7df598f6255..af0dbd8eed74ab 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.test.tsx +++ b/packages/react-components/react-motion-components-preview/library/src/components/Fade/Fade.test.tsx @@ -40,8 +40,12 @@ describe('Fade motion component', () => { // Testing fade out motion rerender({testElement}); expect(animateSpy).toHaveBeenCalledWith( - [{ opacity: 1 }, { opacity: 0 }], - expect.objectContaining({ duration: motionTokens.durationNormal, easing: motionTokens.curveEasyEase }), + [{ opacity: 0 }, { opacity: 1 }], + expect.objectContaining({ + direction: 'reverse', + duration: motionTokens.durationNormal, + easing: motionTokens.curveEasyEase, + }), ); }); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.test.ts b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.test.ts index 1907d7e5eb8f76..2680bc365c1de1 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.test.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.test.ts @@ -1,6 +1,29 @@ -import { expectPresenceMotionFunction, expectPresenceMotionObject } from '../../testing/testUtils'; +import { PresenceComponent } from '@fluentui/react-motion'; +import { expectPresenceMotionFunction, getMotionFunction } from '../../testing/testUtils'; import { Scale } from './Scale'; +export function expectPresenceMotionObject(component: PresenceComponent) { + const presenceMotionFn = getMotionFunction(component); + + // eslint-disable-next-line @nx/workspace-no-restricted-globals + expect(presenceMotionFn?.({ element: document.createElement('div') })).toMatchObject({ + enter: expect.arrayContaining([ + expect.objectContaining({ + duration: expect.any(Number), + easing: expect.any(String), + keyframes: expect.any(Array), + }), + ]), + exit: expect.arrayContaining([ + expect.objectContaining({ + duration: expect.any(Number), + easing: expect.any(String), + keyframes: expect.any(Array), + }), + ]), + }); +} + describe('Scale', () => { it('stores its motion definition as a static function', () => { expectPresenceMotionFunction(Scale); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.ts b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.ts index 9a40cd14319484..4b5fd57b385282 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.ts @@ -1,46 +1,66 @@ -import { - motionTokens, - PresenceMotionFn, - createPresenceComponent, - createPresenceComponentVariant, -} from '@fluentui/react-motion'; +import { motionTokens, createPresenceComponent } from '@fluentui/react-motion'; +import { PresenceMotionFnCreator } from '../../types'; +import { opacityAtom, scaleAtom } from '../Atoms'; +import { ScaleRuntimeParams_unstable, ScaleVariantParams_unstable } from './Scale.types'; /** Define a presence motion for scale in/out */ -const scaleMotion: PresenceMotionFn<{ animateOpacity?: boolean }> = ({ animateOpacity = true }) => { - const fromOpacity = animateOpacity ? 0 : 1; - const toOpacity = 1; - const fromScale = 0.9; // Could be a custom param in the future - const toScale = 1; - - const enterKeyframes = [ - { opacity: fromOpacity, transform: `scale3d(${fromScale}, ${fromScale}, 1)`, visibility: 'visible' }, - { opacity: toOpacity, transform: `scale3d(${toScale}, ${toScale}, 1)` }, - ]; - - const exitKeyframes = [ - { opacity: toOpacity, transform: `scale3d(${toScale}, ${toScale}, 1)` }, - { opacity: fromOpacity, transform: `scale3d(${fromScale}, ${fromScale}, 1)`, visibility: 'hidden' }, - ]; - - return { - enter: { - duration: motionTokens.durationGentle, - easing: motionTokens.curveDecelerateMax, - keyframes: enterKeyframes, - }, - exit: { duration: motionTokens.durationNormal, easing: motionTokens.curveAccelerateMax, keyframes: exitKeyframes }, - }; -}; +export const createScalePresence: PresenceMotionFnCreator = + ({ + enterDuration = motionTokens.durationGentle, + enterEasing = motionTokens.curveEasyEaseMax, + exitDuration = motionTokens.durationNormal, + exitEasing = motionTokens.curveAccelerateMax, + } = {}) => + ({ animateOpacity = true }) => ({ + enter: [ + scaleAtom({ + duration: enterDuration, + easing: enterEasing, + }), + ...(animateOpacity + ? [ + opacityAtom({ + duration: enterDuration, + easing: enterEasing, + }), + ] + : []), + ], + exit: [ + scaleAtom({ + duration: exitDuration, + easing: exitEasing, + direction: 'reverse', + }), + ...(animateOpacity + ? [ + opacityAtom({ + duration: exitDuration, + easing: exitEasing, + direction: 'reverse', + }), + ] + : []), + ], + }); /** A React component that applies scale in/out transitions to its children. */ -export const Scale = createPresenceComponent(scaleMotion); +export const Scale = createPresenceComponent(createScalePresence()); -export const ScaleSnappy = createPresenceComponentVariant(Scale, { - enter: { duration: motionTokens.durationNormal, easing: motionTokens.curveDecelerateMax }, - exit: { duration: motionTokens.durationFast, easing: motionTokens.curveAccelerateMax }, -}); +export const ScaleSnappy = createPresenceComponent( + createScalePresence({ + enterDuration: motionTokens.durationNormal, + enterEasing: motionTokens.curveDecelerateMax, + exitDuration: motionTokens.durationFast, + exitEasing: motionTokens.curveAccelerateMax, + }), +); -export const ScaleRelaxed = createPresenceComponentVariant(Scale, { - enter: { duration: motionTokens.durationSlow, easing: motionTokens.curveDecelerateMax }, - exit: { duration: motionTokens.durationGentle, easing: motionTokens.curveAccelerateMax }, -}); +export const ScaleRelaxed = createPresenceComponent( + createScalePresence({ + enterDuration: motionTokens.durationSlow, + enterEasing: motionTokens.curveDecelerateMax, + exitDuration: motionTokens.durationGentle, + exitEasing: motionTokens.curveAccelerateMax, + }), +); diff --git a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts index ed14a11200efe7..3cb934461f3504 100644 --- a/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts +++ b/packages/react-components/react-motion-components-preview/library/src/components/Scale/Scale.types.ts @@ -1,20 +1,20 @@ -// eslint-disable-next-line @typescript-eslint/naming-convention -export type ScaleRuntimeParams_unstable = { - /** Whether to animate the opacity. Defaults to `true`. */ - animateOpacity?: boolean; -}; - // eslint-disable-next-line @typescript-eslint/naming-convention export type ScaleVariantParams_unstable = { - /** Time (ms) for the enter transition (expand). Defaults to the `durationNormal` value (200 ms). */ - enterScaleDuration?: number; + /** Time (ms) for the enter transition. Defaults to the `durationNormal` value (200 ms). */ + enterDuration?: number; - /** Easing curve for the enter transition (expand). Defaults to the `easeEaseMax` value. */ - enterScaleEasing?: string; + /** Easing curve for the enter transition. Defaults to the `easeEaseMax` value. */ + enterEasing?: string; - /** Time (ms) for the exit transition (collapse). Defaults to the `enterDuration` param for symmetry. */ - exitScaleDuration?: number; + /** Time (ms) for the exit transition. Defaults to the `enterDuration` param for symmetry. */ + exitDuration?: number; - /** Easing curve for the exit transition (collapse). Defaults to the `enterEasing` param for symmetry. */ - exitScaleEasing?: string; + /** Easing curve for the exit transition. Defaults to the `enterEasing` param for symmetry. */ + exitEasing?: string; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export type ScaleRuntimeParams_unstable = { + /** Whether to animate the opacity. Defaults to `true`. */ + animateOpacity?: boolean; }; diff --git a/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts b/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts index 27c00673f66f43..34feb8da2eae5e 100644 --- a/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts +++ b/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts @@ -1,6 +1,6 @@ import type { PresenceComponent, PresenceMotionFn } from '@fluentui/react-motion'; -function getMotionFunction(component: PresenceComponent): PresenceMotionFn | null { +export function getMotionFunction(component: PresenceComponent): PresenceMotionFn | null { const symbols = Object.getOwnPropertySymbols(component); for (const symbol of symbols) {