@react-native-ama/animations 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@react-native-ama/animations",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "react-native": "src/index",
6
+ "types": "dist/index.d.ts",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": [
10
+ {
11
+ "imports": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./dist/index.js"
15
+ ]
16
+ },
17
+ "files": [
18
+ "src",
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "rm -rf dist && tsc -p ./tsconfig.build.json",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@react-native-ama/internal": "*",
27
+ "@react-native-ama/core": "*"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "*",
31
+ "react-native": "*"
32
+ },
33
+ "keywords": [
34
+ "react-native",
35
+ "a11y",
36
+ "accessibility"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/FormidableLabs/react-native-ama",
41
+ "directory": "packages/animations"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/FormidableLabs/react-native-ama/issues"
45
+ },
46
+ "homepage": "https://github.com/FormidableLabs/react-native-ama#readme",
47
+ "publishConfig": {
48
+ "provenance": false
49
+ },
50
+ "author": "",
51
+ "license": "MIT"
52
+ }
@@ -0,0 +1,100 @@
1
+ import { AutofocusContainer } from '@react-native-ama/core';
2
+ import { render } from '@testing-library/react-native';
3
+ import * as React from 'react';
4
+
5
+ import * as UseReanimatedAnimationBuilder from '../hooks/useReanimatedAnimationBuilder';
6
+ import { AnimatedContainer } from './AnimatedContainer';
7
+
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+
11
+ jest.spyOn(console, 'warn').mockImplementation();
12
+ });
13
+
14
+ const from = { left: 0 };
15
+ const to = { opacity: 1 };
16
+
17
+ describe('AnimatedContainer', () => {
18
+ it('passes the given animation data to the hook', () => {
19
+ const useReanimatedAnimationBuilder = jest
20
+ .spyOn(UseReanimatedAnimationBuilder, 'useReanimatedAnimationBuilder')
21
+ .mockReturnValue({ exiting: 'exiting', entering: 'entering' } as any);
22
+
23
+ const { getByTestId } = render(
24
+ <AnimatedContainer testID="test-id" from={from} to={to} />,
25
+ );
26
+
27
+ expect(useReanimatedAnimationBuilder).toHaveBeenCalledWith({
28
+ from,
29
+ to,
30
+ duration: 300,
31
+ exit: undefined,
32
+ });
33
+
34
+ expect(getByTestId('test-id').props).toEqual({
35
+ children: undefined,
36
+ entering: 'entering',
37
+ exiting: 'exiting',
38
+ style: undefined,
39
+ testID: 'test-id',
40
+ });
41
+ });
42
+
43
+ it('passes the given animation data to the hook 2', () => {
44
+ const useReanimatedAnimationBuilder = jest
45
+ .spyOn(UseReanimatedAnimationBuilder, 'useReanimatedAnimationBuilder')
46
+ .mockReturnValue({ exiting: 'exiting', entering: 'entering' } as any);
47
+
48
+ const { getByTestId } = render(
49
+ <AnimatedContainer testID="test-id" from={from} to={to} duration={600} />,
50
+ );
51
+
52
+ expect(useReanimatedAnimationBuilder).toHaveBeenCalledWith({
53
+ from,
54
+ to,
55
+ duration: 600,
56
+ exit: undefined,
57
+ });
58
+
59
+ expect(getByTestId('test-id').props).toEqual({
60
+ children: undefined,
61
+ entering: 'entering',
62
+ exiting: 'exiting',
63
+ style: undefined,
64
+ testID: 'test-id',
65
+ });
66
+ });
67
+
68
+ it('wraps the content with the AutofocusContainer when the autofocus property is true', () => {
69
+ const { UNSAFE_getAllByType } = render(
70
+ <AnimatedContainer
71
+ testID="test-id"
72
+ from={from}
73
+ to={to}
74
+ duration={300}
75
+ autofocus
76
+ />,
77
+ );
78
+
79
+ expect(UNSAFE_getAllByType(AutofocusContainer)).toBeDefined();
80
+ });
81
+ });
82
+
83
+ jest.mock('../hooks/useReanimatedAnimationBuilder');
84
+ jest.mock('../hooks/useFocus', () => {
85
+ return {
86
+ useFocus: () => {
87
+ return {
88
+ setFocus: jest.fn(),
89
+ };
90
+ },
91
+ };
92
+ });
93
+
94
+ jest.mock('react-native-reanimated', () => {
95
+ const { View } = require('react-native');
96
+
97
+ return {
98
+ View: (props: any) => <View {...props} />,
99
+ };
100
+ });
@@ -0,0 +1,64 @@
1
+ import { AutofocusContainer } from '@react-native-ama/core';
2
+ import * as React from 'react';
3
+ import type { PropsWithChildren } from 'react';
4
+ import type { ViewProps } from 'react-native';
5
+ import Animated, { AnimateProps } from 'react-native-reanimated';
6
+
7
+ import {
8
+ AnimatedEntryViewStyle,
9
+ AnimatedExitViewStyle,
10
+ ToAnimation,
11
+ useReanimatedAnimationBuilder,
12
+ } from '../hooks/useReanimatedAnimationBuilder';
13
+
14
+ type UseReanimated = Omit<AnimateProps<ViewProps>, 'entering' | 'exiting'> & {
15
+ autofocus?: boolean;
16
+ duration?: number;
17
+ from: AnimatedEntryViewStyle;
18
+ exitFrom?: AnimatedExitViewStyle;
19
+ exitTo?: ToAnimation;
20
+ to: ToAnimation;
21
+ style?: Pick<ViewProps, 'style'>;
22
+ };
23
+
24
+ export const AnimatedContainer = React.forwardRef<
25
+ Animated.View,
26
+ PropsWithChildren<UseReanimated>
27
+ >(
28
+ (
29
+ {
30
+ from,
31
+ to,
32
+ exitFrom,
33
+ exitTo,
34
+ duration = 300,
35
+ style,
36
+ autofocus,
37
+ children,
38
+ ...rest
39
+ },
40
+ forwardRef,
41
+ ) => {
42
+ const { entering, exiting } = useReanimatedAnimationBuilder({
43
+ from,
44
+ to,
45
+ exitFrom,
46
+ exitTo,
47
+ duration,
48
+ });
49
+
50
+ const Wrapper = autofocus ? AutofocusContainer : React.Fragment;
51
+ return (
52
+ <Wrapper>
53
+ <Animated.View
54
+ style={style}
55
+ entering={entering}
56
+ exiting={exiting}
57
+ ref={forwardRef}
58
+ {...rest}>
59
+ {children}
60
+ </Animated.View>
61
+ </Wrapper>
62
+ );
63
+ },
64
+ );
@@ -0,0 +1,162 @@
1
+ import * as AMAProvider from '@react-native-ama/core';
2
+ import { renderHook } from '@testing-library/react-native';
3
+ import { Animated } from 'react-native';
4
+
5
+ import { useAnimation } from './useAnimation';
6
+
7
+ beforeEach(() => {
8
+ jest.clearAllMocks();
9
+ });
10
+
11
+ let timing = jest.spyOn(Animated, 'timing');
12
+
13
+ describe('useAnimation', () => {
14
+ describe('Given isReduceMotionEnabled is false', () => {
15
+ beforeEach(() => {
16
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
17
+ ...amaContextValues,
18
+ isReduceMotionEnabled: false,
19
+ } as any);
20
+ });
21
+
22
+ it('Then plays only the main timing', () => {
23
+ const { result } = renderHook(() =>
24
+ useAnimation({
25
+ duration: 300,
26
+ useNativeDriver: false,
27
+ from: {
28
+ left: 0,
29
+ opacity: 0,
30
+ },
31
+ to: {
32
+ left: 100,
33
+ opacity: 1,
34
+ },
35
+ }),
36
+ );
37
+
38
+ result.current.play();
39
+
40
+ expect(timing).toHaveBeenCalledTimes(1);
41
+ expect(timing).toHaveBeenCalledWith(expect.any(Animated.Value), {
42
+ duration: 300,
43
+ toValue: 1,
44
+ useNativeDriver: false,
45
+ });
46
+ });
47
+ });
48
+
49
+ describe('Given isReduceMotionEnabled is true', () => {
50
+ beforeEach(() => {
51
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
52
+ ...amaContextValues,
53
+ isReduceMotionEnabled: true,
54
+ } as any);
55
+ });
56
+
57
+ it('Then plays the reduceMotion and the main progress', () => {
58
+ const { result } = renderHook(() =>
59
+ useAnimation({
60
+ duration: 300,
61
+ useNativeDriver: false,
62
+ from: {
63
+ left: 0,
64
+ opacity: 0,
65
+ },
66
+ to: {
67
+ left: 100,
68
+ opacity: 1,
69
+ },
70
+ }),
71
+ );
72
+
73
+ result.current.play();
74
+
75
+ expect(timing).toHaveBeenCalledTimes(2);
76
+ expect(timing).toHaveBeenNthCalledWith(1, expect.any(Animated.Value), {
77
+ duration: 0,
78
+ toValue: 1,
79
+ useNativeDriver: false,
80
+ });
81
+ expect(timing).toHaveBeenNthCalledWith(2, expect.any(Animated.Value), {
82
+ duration: 300,
83
+ toValue: 1,
84
+ useNativeDriver: false,
85
+ });
86
+ });
87
+
88
+ it('Then plays the main progress with duration 0 when is a motion-only animation', () => {
89
+ const { result } = renderHook(() =>
90
+ useAnimation({
91
+ duration: 300,
92
+ useNativeDriver: false,
93
+ from: {
94
+ left: 0,
95
+ },
96
+ to: {
97
+ left: 100,
98
+ },
99
+ }),
100
+ );
101
+
102
+ result.current.play();
103
+
104
+ expect(timing).toHaveBeenCalledTimes(2);
105
+ expect(timing).toHaveBeenNthCalledWith(1, expect.any(Animated.Value), {
106
+ duration: 0,
107
+ toValue: 1,
108
+ useNativeDriver: false,
109
+ });
110
+ expect(timing).toHaveBeenNthCalledWith(2, expect.any(Animated.Value), {
111
+ duration: 0,
112
+ toValue: 1,
113
+ useNativeDriver: false,
114
+ });
115
+ });
116
+
117
+ it('Then plays the main progress with duration 0 when skipIfReduceMotionEnabled is set to true', () => {
118
+ const { result } = renderHook(() =>
119
+ useAnimation({
120
+ duration: 300,
121
+ useNativeDriver: false,
122
+ skipIfReduceMotionEnabled: true,
123
+ from: {
124
+ opacity: 0,
125
+ },
126
+ to: {
127
+ opacity: 100,
128
+ },
129
+ }),
130
+ );
131
+
132
+ result.current.play();
133
+
134
+ expect(timing).toHaveBeenCalledTimes(2);
135
+ expect(timing).toHaveBeenNthCalledWith(1, expect.any(Animated.Value), {
136
+ duration: 0,
137
+ toValue: 1,
138
+ useNativeDriver: false,
139
+ });
140
+ expect(timing).toHaveBeenNthCalledWith(2, expect.any(Animated.Value), {
141
+ duration: 0,
142
+ toValue: 1,
143
+ useNativeDriver: false,
144
+ });
145
+ });
146
+ });
147
+ });
148
+
149
+ const amaContextValues = {
150
+ isReduceTransparencyEnabled: false,
151
+ isBoldTextEnabled: false,
152
+ isGrayscaleEnabled: false,
153
+ isInvertColorsEnabled: false,
154
+ isReduceMotionEnabled: false,
155
+ isScreenReaderEnabled: false,
156
+ reactNavigationScreenOptions: {
157
+ animationEnabled: true,
158
+ animation: 'default',
159
+ },
160
+ };
161
+
162
+ jest.mock('@react-native-ama/core');
@@ -0,0 +1,64 @@
1
+ import { useAMAContext } from '@react-native-ama/core';
2
+ import { interpolateAnimationStates } from '@react-native-ama/internal';
3
+ import * as React from 'react';
4
+ import type { ViewStyle } from 'react-native';
5
+ import { Animated } from 'react-native';
6
+
7
+ type UseAnimation = {
8
+ from: ViewStyle;
9
+ to: ViewStyle;
10
+ duration: number;
11
+ useNativeDriver: boolean;
12
+ skipIfReduceMotionEnabled?: boolean;
13
+ };
14
+
15
+ export const useAnimation = ({
16
+ duration,
17
+ useNativeDriver,
18
+ from,
19
+ to,
20
+ skipIfReduceMotionEnabled = false,
21
+ }: UseAnimation) => {
22
+ const { isReduceMotionEnabled } = useAMAContext();
23
+ const progress = React.useRef(new Animated.Value(0)).current;
24
+ const reduceMotionProgress = React.useRef(new Animated.Value(0)).current;
25
+ const hasOnlyMotionAnimation = React.useRef(false);
26
+
27
+ const play = (toValue: number = 1) => {
28
+ if (isReduceMotionEnabled) {
29
+ Animated.timing(reduceMotionProgress, {
30
+ duration: 0,
31
+ toValue,
32
+ useNativeDriver,
33
+ }).start();
34
+ }
35
+
36
+ const animationDuration =
37
+ isReduceMotionEnabled &&
38
+ (hasOnlyMotionAnimation.current || skipIfReduceMotionEnabled)
39
+ ? 0
40
+ : duration;
41
+
42
+ return Animated.timing(progress, {
43
+ duration: animationDuration,
44
+ toValue,
45
+ useNativeDriver,
46
+ });
47
+ };
48
+
49
+ const { __hasOnlyMotionAnimation, ...style } = interpolateAnimationStates(
50
+ from,
51
+ to,
52
+ isReduceMotionEnabled,
53
+ progress,
54
+ reduceMotionProgress,
55
+ );
56
+
57
+ hasOnlyMotionAnimation.current = __hasOnlyMotionAnimation;
58
+
59
+ return {
60
+ animatedStyle: style,
61
+ progress,
62
+ play,
63
+ };
64
+ };
@@ -0,0 +1,39 @@
1
+ import * as AMAProvider from '@react-native-ama/core';
2
+ import { renderHook } from '@testing-library/react-native';
3
+
4
+ import { useAnimationDuration } from './useAnimationDuration';
5
+
6
+ describe('useAnimationDuration', () => {
7
+ it('returns the given duration for motion animation when isReduceMotionEnabled is false', () => {
8
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
9
+ ...amaContextValues,
10
+ isReduceMotionEnabled: false,
11
+ } as any);
12
+
13
+ const { result } = renderHook(() => useAnimationDuration());
14
+
15
+ expect(result.current.getAnimationDuration('left', 100)).toBe(100);
16
+ });
17
+
18
+ it('returns 0 for motion animation when isReduceMotionEnabled is true', () => {
19
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
20
+ ...amaContextValues,
21
+ isReduceMotionEnabled: true,
22
+ } as any);
23
+
24
+ const { result } = renderHook(() => useAnimationDuration());
25
+
26
+ expect(result.current.getAnimationDuration('left', 100)).toBe(0);
27
+ });
28
+ });
29
+
30
+ const amaContextValues = {
31
+ isReduceTransparencyEnabled: false,
32
+ isBoldTextEnabled: false,
33
+ isGrayscaleEnabled: false,
34
+ isInvertColorsEnabled: false,
35
+ isReduceMotionEnabled: false,
36
+ isScreenReaderEnabled: false,
37
+ };
38
+
39
+ jest.mock('@react-native-ama/core');
@@ -0,0 +1,22 @@
1
+ import { useAMAContext } from '@react-native-ama/core';
2
+ import React from 'react';
3
+ import type { ViewStyle } from 'react-native';
4
+
5
+ import { isMotionAnimation } from '../utils/isMotionAnimation';
6
+
7
+ export const useAnimationDuration = () => {
8
+ const { isReduceMotionEnabled } = useAMAContext();
9
+
10
+ const getAnimationDuration = React.useCallback(
11
+ (propertyToAnimate: keyof ViewStyle, duration: number) => {
12
+ return isReduceMotionEnabled && isMotionAnimation(propertyToAnimate)
13
+ ? 0
14
+ : duration;
15
+ },
16
+ [isReduceMotionEnabled],
17
+ );
18
+
19
+ return {
20
+ getAnimationDuration,
21
+ };
22
+ };
@@ -0,0 +1,155 @@
1
+ import * as AMAProvider from '@react-native-ama/core';
2
+ import { renderHook } from '@testing-library/react-native';
3
+
4
+ import { useReanimatedAnimationBuilder } from './useReanimatedAnimationBuilder';
5
+
6
+ beforeEach(() => {
7
+ jest.clearAllMocks();
8
+ });
9
+
10
+ let withTiming: jest.Mock;
11
+
12
+ describe('useReanimatedAnimationBuilder', () => {
13
+ describe('entering animation', () => {
14
+ it('handles the custom reanimated params', () => {
15
+ const { result } = renderHook(() =>
16
+ useReanimatedAnimationBuilder({
17
+ from: { transform: [{ translateY: 'targetHeight' }], opacity: 0 },
18
+ to: {
19
+ transform: [{ translateY: -42 }],
20
+ opacity: 1,
21
+ },
22
+ duration: 300,
23
+ }),
24
+ );
25
+
26
+ expect(
27
+ result.current.entering({
28
+ targetHeight: 42,
29
+ } as any),
30
+ ).toEqual({
31
+ animations: {
32
+ opacity: 1,
33
+ transform: [{ translateY: -42 }],
34
+ },
35
+ initialValues: { opacity: 0, transform: [{ translateY: 42 }] },
36
+ });
37
+ });
38
+
39
+ it('generates the animation with duration 0 for motion ones', () => {
40
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
41
+ isReduceMotionEnabled: true,
42
+ } as any);
43
+
44
+ const { result } = renderHook(() =>
45
+ useReanimatedAnimationBuilder({
46
+ from: { transform: [{ translateY: 'targetHeight' }], opacity: 0 },
47
+ to: {
48
+ transform: [{ translateY: -42 }],
49
+ opacity: 1,
50
+ },
51
+ duration: 300,
52
+ }),
53
+ );
54
+
55
+ result.current.entering({
56
+ targetHeight: 42,
57
+ } as any);
58
+
59
+ expect(withTiming).toHaveBeenCalledTimes(2);
60
+ expect(withTiming).toHaveBeenNthCalledWith(1, -42, {
61
+ duration: 0,
62
+ });
63
+ expect(withTiming).toHaveBeenNthCalledWith(2, 1, {
64
+ duration: 300,
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('exiting animation', () => {
70
+ it('reverse the from/to animation', () => {
71
+ const { result } = renderHook(() =>
72
+ useReanimatedAnimationBuilder({
73
+ from: { transform: [{ translateY: 'targetHeight' }], opacity: 0 },
74
+ to: {
75
+ transform: [{ translateY: -42 }],
76
+ opacity: 1,
77
+ },
78
+ duration: 300,
79
+ }),
80
+ );
81
+
82
+ expect(
83
+ result.current.exiting({
84
+ targetHeight: 42,
85
+ } as any),
86
+ ).toEqual({
87
+ animations: {
88
+ opacity: 0,
89
+ transform: [{ translateY: 42 }],
90
+ },
91
+ initialValues: { opacity: 1, transform: [{ translateY: -42 }] },
92
+ });
93
+ });
94
+
95
+ it('allows specifying a different start animation', () => {
96
+ const { result } = renderHook(() =>
97
+ useReanimatedAnimationBuilder({
98
+ from: { transform: [{ translateY: 'targetHeight' }], opacity: 0 },
99
+ exitFrom: {
100
+ transform: [{ translateY: 0 }],
101
+ opacity: 0.5,
102
+ },
103
+ to: { transform: [{ translateY: -42 }], opacity: 1 },
104
+ duration: 300,
105
+ }),
106
+ );
107
+
108
+ expect(
109
+ result.current.exiting({
110
+ targetHeight: 42,
111
+ } as any),
112
+ ).toEqual({
113
+ animations: {
114
+ opacity: 0.5,
115
+ transform: [{ translateY: 0 }],
116
+ },
117
+ initialValues: { opacity: 1, transform: [{ translateY: -42 }] },
118
+ });
119
+ });
120
+
121
+ it('generates the animation with duration 0 for motion ones', () => {
122
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
123
+ isReduceMotionEnabled: true,
124
+ } as any);
125
+
126
+ const { result } = renderHook(() =>
127
+ useReanimatedAnimationBuilder({
128
+ from: { transform: [{ translateY: 'targetHeight' }], opacity: 0 },
129
+ to: { transform: [{ translateY: -42 }], opacity: 1 },
130
+ duration: 300,
131
+ }),
132
+ );
133
+
134
+ result.current.exiting({ targetHeight: 42 } as any);
135
+
136
+ expect(withTiming).toHaveBeenCalledTimes(2);
137
+ expect(withTiming).toHaveBeenNthCalledWith(1, 42, {
138
+ duration: 0,
139
+ });
140
+ expect(withTiming).toHaveBeenNthCalledWith(2, 0, {
141
+ duration: 300,
142
+ });
143
+ });
144
+ });
145
+ });
146
+
147
+ function mockReanimated() {
148
+ withTiming = jest.fn(value => value);
149
+
150
+ return {
151
+ withTiming,
152
+ };
153
+ }
154
+
155
+ jest.mock('react-native-reanimated', () => mockReanimated());
@@ -0,0 +1,187 @@
1
+ import { useAMAContext } from '@react-native-ama/core';
2
+ import { MOTION_ANIMATIONS } from '@react-native-ama/internal';
3
+ import type { ViewProps, ViewStyle } from 'react-native';
4
+ import {
5
+ EntryAnimationsValues,
6
+ EntryExitAnimationFunction,
7
+ ExitAnimationsValues,
8
+ StyleProps,
9
+ withTiming,
10
+ } from 'react-native-reanimated';
11
+
12
+ export type ToAnimation = ViewProps['style'];
13
+
14
+ type UseReanimatedAnimationBuilder = {
15
+ from: AnimatedEntryViewStyle;
16
+ to: ToAnimation;
17
+ exitFrom?: AnimatedExitViewStyle;
18
+ exitTo?: ToAnimation;
19
+ duration: number;
20
+ };
21
+
22
+ export const useReanimatedAnimationBuilder = ({
23
+ from,
24
+ to,
25
+ exitFrom,
26
+ exitTo,
27
+ duration,
28
+ }: UseReanimatedAnimationBuilder) => {
29
+ const { isReduceMotionEnabled } = useAMAContext();
30
+
31
+ const animationBuilder = <InitialValues, FinalValues>(
32
+ initial: InitialValues,
33
+ final: FinalValues,
34
+ ): EntryExitAnimationFunction => {
35
+ return (values: EntryAnimationsValues | ExitAnimationsValues) => {
36
+ 'worklet';
37
+
38
+ const animations = (function generateAnimations(startValues, endValues) {
39
+ return Object.keys(startValues as object).reduce(
40
+ (outputAnimation, key) => {
41
+ const k: keyof InitialValues = key as keyof InitialValues;
42
+ const toValue = endValues[key as keyof FinalValues];
43
+ const value = startValues[k];
44
+ const mappedToValue =
45
+ // @ts-ignore
46
+ typeof toValue === 'string' ? values[toValue] || 0 : toValue;
47
+
48
+ const realDuration =
49
+ isReduceMotionEnabled &&
50
+ MOTION_ANIMATIONS.includes(key as keyof ViewStyle)
51
+ ? 0
52
+ : duration;
53
+ const newValue = Array.isArray(value)
54
+ ? value.map((item, index) =>
55
+ generateAnimations(
56
+ item as InitialValues,
57
+ // @ts-ignore
58
+ endValues?.[k]?.[index],
59
+ ),
60
+ )
61
+ : withTiming(mappedToValue, {
62
+ duration: realDuration,
63
+ });
64
+
65
+ outputAnimation[k] = newValue;
66
+
67
+ return outputAnimation;
68
+ },
69
+ {} as InitialValues,
70
+ );
71
+ })(initial, final);
72
+
73
+ const initialValues = (function generateInitialValues(
74
+ startValues,
75
+ ): StyleProps {
76
+ return Object.entries(startValues as object).reduce(
77
+ (newList, [key, value]) => {
78
+ const mappedValue =
79
+ // @ts-ignore
80
+ typeof value === 'string' ? values[value] : value;
81
+ const newValue = Array.isArray(value)
82
+ ? value.map(item => generateInitialValues(item as InitialValues))
83
+ : mappedValue;
84
+
85
+ newList[key as keyof StyleProps] = newValue;
86
+
87
+ return newList;
88
+ },
89
+ {} as StyleProps,
90
+ );
91
+ })(initial);
92
+
93
+ return {
94
+ initialValues,
95
+ animations,
96
+ };
97
+ };
98
+ };
99
+
100
+ return {
101
+ entering: animationBuilder<AnimatedEntryViewStyle, ToAnimation>(from, to),
102
+ exiting: animationBuilder<
103
+ ToAnimation,
104
+ AnimatedExitViewStyle | AnimatedEntryViewStyle
105
+ >(exitTo || to, exitFrom || from),
106
+ };
107
+ };
108
+
109
+ export type AnimatedEntryViewStyle = ReanimatedStyle<ReanimatedEntryValues>;
110
+ export type AnimatedExitViewStyle = ReanimatedStyle<ReanimatedExitValues>;
111
+
112
+ type ReanimatedStyle<K> = {
113
+ [key in keyof Omit<ViewStyle, 'transform'>]: ViewStyle[key] | K;
114
+ } & {
115
+ transform?:
116
+ | (
117
+ | PerpectiveTransform<K>
118
+ | RotateTransform<K>
119
+ | RotateXTransform<K>
120
+ | RotateYTransform<K>
121
+ | RotateZTransform<K>
122
+ | ScaleTransform<K>
123
+ | ScaleXTransform<K>
124
+ | ScaleYTransform<K>
125
+ | TranslateXTransform<K>
126
+ | TranslateYTransform<K>
127
+ | SkewXTransform<K>
128
+ | SkewYTransform<K>
129
+ | MatrixTransform<K>
130
+ )[]
131
+ | undefined;
132
+ };
133
+
134
+ type PerpectiveTransform<K> = {
135
+ c: string | K;
136
+ };
137
+
138
+ type RotateTransform<K> = {
139
+ rotate: string | K;
140
+ };
141
+
142
+ type RotateXTransform<K> = {
143
+ rotateX: string | K;
144
+ };
145
+
146
+ type RotateYTransform<K> = {
147
+ rotateY: string | K;
148
+ };
149
+
150
+ type RotateZTransform<K> = {
151
+ rotateZ: string | K;
152
+ };
153
+
154
+ type ScaleTransform<K> = {
155
+ scale: number | K;
156
+ };
157
+
158
+ type ScaleXTransform<K> = {
159
+ scaleX: number | K;
160
+ };
161
+
162
+ type ScaleYTransform<K> = {
163
+ scaleY: number | K;
164
+ };
165
+
166
+ type TranslateXTransform<K> = {
167
+ translateX: number | K;
168
+ };
169
+
170
+ type TranslateYTransform<K> = {
171
+ translateY: number | K;
172
+ };
173
+
174
+ type SkewXTransform<K> = {
175
+ skewX: string | K;
176
+ };
177
+
178
+ type SkewYTransform<K> = {
179
+ skewY: string | K;
180
+ };
181
+
182
+ type MatrixTransform<K> = {
183
+ matrix: number[] | K;
184
+ };
185
+
186
+ type ReanimatedEntryValues = keyof EntryAnimationsValues;
187
+ type ReanimatedExitValues = keyof ExitAnimationsValues;
@@ -0,0 +1,96 @@
1
+ import * as AMAProvider from '@react-native-ama/core';
2
+ import { MOTION_ANIMATIONS } from '@react-native-ama/internal';
3
+ import { renderHook } from '@testing-library/react-native';
4
+
5
+ import { useReanimatedTiming } from './useReanimatedTiming';
6
+
7
+ beforeEach(() => {
8
+ jest.clearAllMocks();
9
+ });
10
+
11
+ describe('useReanimatedTiming', () => {
12
+ describe('withTiming', () => {
13
+ it('calls reanimated withTiming function with the give parameters when isReduceMotionEnabled is false', () => {
14
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
15
+ ...amaContextValues,
16
+ isReduceMotionEnabled: false,
17
+ } as any);
18
+
19
+ const { result } = renderHook(() => useReanimatedTiming());
20
+
21
+ result.current.withTiming('left', 42, { duration: 100 });
22
+ expect(withTiming).toHaveBeenCalledWith(42, { duration: 100 }, undefined);
23
+ });
24
+
25
+ it.each(MOTION_ANIMATIONS)(
26
+ 'calls reanimated withTiming function with duration = 0 for motion animation when isReduceMotionEnabled is true',
27
+ key => {
28
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
29
+ ...amaContextValues,
30
+ isReduceMotionEnabled: true,
31
+ } as any);
32
+
33
+ const { result } = renderHook(() => useReanimatedTiming());
34
+
35
+ result.current.withTiming(key as any, 42, { duration: 100 });
36
+ expect(withTiming).toHaveBeenCalledWith(42, { duration: 0 }, undefined);
37
+ },
38
+ );
39
+ });
40
+
41
+ describe('withSpring', () => {
42
+ it('calls reanimated withSpring function with the give parameters when isReduceMotionEnabled is false', () => {
43
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
44
+ ...amaContextValues,
45
+ isReduceMotionEnabled: false,
46
+ } as any);
47
+
48
+ const { result } = renderHook(() => useReanimatedTiming());
49
+
50
+ result.current.withSpring('left', 42, { damping: 42 });
51
+ expect(withSpring).toHaveBeenCalledWith(42, { damping: 42 }, undefined);
52
+ });
53
+
54
+ it.each(MOTION_ANIMATIONS)(
55
+ 'calls reanimated withTiming function with duration = 0 for motion animation when isReduceMotionEnabled is true',
56
+ key => {
57
+ jest.spyOn(AMAProvider, 'useAMAContext').mockReturnValue({
58
+ ...amaContextValues,
59
+ isReduceMotionEnabled: true,
60
+ } as any);
61
+
62
+ const { result } = renderHook(() => useReanimatedTiming());
63
+
64
+ result.current.withSpring(key as any, 42);
65
+
66
+ expect(withSpring).not.toHaveBeenCalled();
67
+ expect(withTiming).toHaveBeenCalledWith(42, { duration: 0 });
68
+ },
69
+ );
70
+ });
71
+ });
72
+
73
+ const amaContextValues = {
74
+ isReduceTransparencyEnabled: false,
75
+ isBoldTextEnabled: false,
76
+ isGrayscaleEnabled: false,
77
+ isInvertColorsEnabled: false,
78
+ isReduceMotionEnabled: false,
79
+ isScreenReaderEnabled: false,
80
+ };
81
+
82
+ let withSpring: jest.Mock;
83
+ let withTiming: jest.Mock;
84
+
85
+ function mockReanimated() {
86
+ withSpring = jest.fn();
87
+ withTiming = jest.fn();
88
+
89
+ return {
90
+ withSpring,
91
+ withTiming,
92
+ };
93
+ }
94
+
95
+ jest.mock('@react-native-ama/core');
96
+ jest.mock('react-native-reanimated', () => mockReanimated());
@@ -0,0 +1,53 @@
1
+ import { useAMAContext } from '@react-native-ama/core';
2
+ import type { MotionAnimationKey } from '@react-native-ama/internal';
3
+ import React from 'react';
4
+ import {
5
+ AnimationCallback,
6
+ withSpring as ReanimateWithSpring,
7
+ withTiming as ReanimateWithTiming,
8
+ WithSpringConfig,
9
+ WithTimingConfig,
10
+ } from 'react-native-reanimated';
11
+
12
+ import { isMotionAnimation } from '../utils/isMotionAnimation';
13
+
14
+ export const useReanimatedTiming = () => {
15
+ const { isReduceMotionEnabled } = useAMAContext();
16
+
17
+ const withTiming = React.useCallback(
18
+ (
19
+ propertyKey: MotionAnimationKey,
20
+ toValue: number,
21
+ config: WithTimingConfig = {},
22
+ callback?: AnimationCallback,
23
+ ) => {
24
+ if (isReduceMotionEnabled && isMotionAnimation(propertyKey)) {
25
+ config.duration = 0;
26
+ }
27
+
28
+ return ReanimateWithTiming(toValue, config, callback);
29
+ },
30
+ [isReduceMotionEnabled],
31
+ );
32
+
33
+ const withSpring = React.useCallback(
34
+ (
35
+ propertyKey: MotionAnimationKey,
36
+ toValue: number,
37
+ config?: WithSpringConfig,
38
+ callback?: AnimationCallback,
39
+ ) => {
40
+ if (isReduceMotionEnabled && isMotionAnimation(propertyKey)) {
41
+ return ReanimateWithTiming(toValue, { duration: 0 });
42
+ }
43
+
44
+ return ReanimateWithSpring(toValue, config, callback);
45
+ },
46
+ [isReduceMotionEnabled],
47
+ );
48
+
49
+ return {
50
+ withTiming,
51
+ withSpring,
52
+ };
53
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // Components
2
+ export { AnimatedContainer } from './components/AnimatedContainer';
3
+
4
+ // Hooks
5
+ export { useAnimation } from './hooks/useAnimation';
6
+ export { useAnimationDuration } from './hooks/useAnimationDuration';
7
+ export { useReanimatedAnimationBuilder } from './hooks/useReanimatedAnimationBuilder';
8
+ export { useReanimatedTiming } from './hooks/useReanimatedTiming';
@@ -0,0 +1,8 @@
1
+ import {
2
+ MOTION_ANIMATIONS,
3
+ type MotionAnimationKey,
4
+ } from '@react-native-ama/internal';
5
+
6
+ export const isMotionAnimation = (key: MotionAnimationKey) => {
7
+ return MOTION_ANIMATIONS.includes(key);
8
+ };