diff --git a/packages/animations/docs/components/AnimatedContainer.md b/packages/animations/docs/components/AnimatedContainer.md new file mode 100644 index 00000000..12de6d0b --- /dev/null +++ b/packages/animations/docs/components/AnimatedContainer.md @@ -0,0 +1,155 @@ +import { Required } from '@site/src/components' + +# AnimatedContainer + +This component is built on top of [Reanimated custom animations](https://docs.swmansion.com/react-native-reanimated/docs/api/LayoutAnimations/customAnimations) +and let us animate the entry and exitFrom of the container, honouring +the [Reduce Motion] (https://reactnative.dev/docs/accessibilityinfo) preference. + +## Example + +```jsx +import { AnimatedContainer } from '@react-native-ama/animations'; + +; +``` + +When the component is mounted, it will animate it from: `from -> to`, and when it is unmounted, it will animate it from: `exitFrom -> to`. +If the property `exitFrom from` is not specified, it will then play the animation in reverse: `to -> from`. + +## Accessibility + +For both, enter and exitFrom animation, the component will use a `duration={0}` for each [motion property](../utils/isMotionAnimation) when [Reduce Motion](../hooks/useAMAContext#isreducemotionenabled) option is enabled. + +## Props + +### `autofocus` + +If set to true, wraps the child inside the [AutofocusContainer](./AutofocusContainer) + +| Type | Default | +| ------- | ------- | +| boolean | false | + +### `duration` + +The preferred animation duration. + +| Type | Default | +| ------ | ------- | +| number | 300 | + +:::note + +The component will use a `duration={0}` for each [motion property](../utils/isMotionAnimation) when [Reduce Motion](../hooks/useAMAContext#isreducemotionenabled) option is enabled. + +::: + +### `from` + +The initial value of the animation. + +| Type | +| ---------------------------------- | +| ViewStyle \| ReanimatedEntryValues | + +This parameter sets the initial values for the [Reanimated custom animations](https://docs.swmansion.com/react-native-reanimated/docs/api/LayoutAnimations/customAnimations). +In additional to `ViewStyle`, this property also allows access to special values available by Reanimated: + +| Value | Description | +| ------------------- | ------------------------------------------------------------- | +| targetOriginX | X coordinate of top left corner in parent's coordinate system | +| targetOriginY | Y coordinate of top left corner in parent's coordinate system | +| targetWidth | view's width | +| targetHeight | view's height | +| targetGlobalOriginX | X coordinate of top left corner in global coordinate system | +| targetGlobalOriginY | Y coordinate of top left corner in global coordinate system | + +#### Example + +```jsx +import { AnimatedContainer } from 'react-native-ama'; + +; +``` + +In this example, `translateY` is assigned the value of the view's height when the container is animated. + +### `to` + +The final (or initial) value of the animation. + +| Type | +| --------- | +| ViewStyle | + +This value is used for both entering and exiting animation. +For the entering animation, this is used as the final state; for the exitFrom one, as the initial state. + +```jsx +import { AnimatedContainer } from 'react-native-ama'; + +; +``` + +In this case, the view will fade In when mounted and fade out when unmounted. + +### `exitFrom` + +The initial value for the unmounting animation. + +| Type | +| --------------------------------- | +| ViewStyle \| ReanimatedExitValues | + +In additional to `ViewStyle` this property also allows to access to special values available by Reanimated: + +| Value | Description | +| -------------------- | ------------------------------------------------------------- | +| currentOriginX | X coordinate of top left corner in parent's coordinate system | +| currentOriginY | Y coordinate of top left corner in parent's coordinate system | +| currentWidth | view's width | +| currentHeight | view's height | +| currentGlobalOriginX | X coordinate of top left corner in global coordinate system | +| currentGlobalOriginY | Y coordinate of top left corner in global coordinate system | + +```jsx +import { AnimatedContainer } from 'react-native-ama'; + +; +``` + +Because in the [from](#from) animation, we did specify the special value **targetHeight** we need to provide a "different" value for the exiting animation +as that special name does not exist for the exitFrom animation. + +:::note + +If not specified, the [from](#from) value is used as the final one for the unmounting animation. + +::: + +### `style` + +The container style + +| Type | +| --------- | +| ViewStyle | + +## Related guidelines + +- [Animations](../guidelines/animations) diff --git a/packages/animations/docs/components/_category_.json b/packages/animations/docs/components/_category_.json new file mode 100644 index 00000000..92f583da --- /dev/null +++ b/packages/animations/docs/components/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Components", + "collapsible": true, + "collapsed": false +} diff --git a/packages/animations/docs/hooks/_category_.json b/packages/animations/docs/hooks/_category_.json new file mode 100644 index 00000000..15dc8a9e --- /dev/null +++ b/packages/animations/docs/hooks/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Hooks", + "collapsible": true, + "collapsed": false +} diff --git a/packages/animations/docs/hooks/useAnimation.mdx b/packages/animations/docs/hooks/useAnimation.mdx new file mode 100644 index 00000000..f948526c --- /dev/null +++ b/packages/animations/docs/hooks/useAnimation.mdx @@ -0,0 +1,252 @@ +import ReactPlayer from 'react-player'; + +# useAnimation + +`useAnimation` is a hook that helps create accessible animations by honouring the user [Reduce Motion](/core/hooks/useAMAContext#isreducemotionenabled) preference. + +:::note + +This hook uses the react-native built-in [Animated API](https://reactnative.dev/docs/animations), [check here](./useReanimatedTiming.md) for accessible animations in Reanimated. +::: + +## Usage + +```ts +import { useAnimation } from 'react-native-ama'; + +const { animatedStyle, progress, play } = useAnimation({ + duration, + useNativeDriver, + from: {}, + to: {}, + skipIfReduceMotionEnabled, +}); +``` + +| Property | Type | Description | +|---------------------------|-----------|-----------------------------------------------------------------------------------------------------| +| duration | number | the animation duration | +| useNativeDriver | boolean | [Using the native driver](https://reactnative.dev/docs/animations#using-the-native-driver) | +| from | ViewStyle | the initial state of the animation | +| to | ViewStyle | the final state of the animation | +| skipIfReduceMotionEnabled | boolean | _(Optional)_ if true, the animation will be played with duration 0 when `Reduce Motion` is enabled. | + +### Returns + +| Property | Type | Description | +|---------------|----------------------------------|--------------------------------------------------| +| animatedStyle | Object | the animation style to apply at the component | +| progress | Animated.Value | The Animated.Value used to trigger the animation | +| play | (toValue = 1) => Animated.timing | Returns the Animated.timing | + +## Example + +The following animations translate in, with fading, an absolute positioned view: + +```ts +import React, { useRef } from 'react'; +import { Animated, Dimensions, StyleSheet, View } from 'react-native'; +import { Pressable, Text } from 'react-native-ama'; +import { useAccessibleAnimation } from 'react-native-ama'; + +export const ReduceMotionScreen = () => { + const [overlayProgressValue, setOverlayProgressValue] = + React.useState(null); + const animationProgress = useRef( + new Animated.Value(0), + ).current; + const { play, animatedStyle, progress } = useAccessibleAnimation({ + duration: 300, + useNativeDriver: true, + from: { + opacity: 0, + transform: [{ translateY: 200 }], + }, + to: { + opacity: 1, + transform: [{ translateY: 0 }], + }, + }); + + const overlayStyle = { + opacity: overlayProgressValue || 0, + }; + + const playAnimation = () => { + setOverlayProgressValue(progress); + play().start(); + }; + + return ( + <> + + + + {overlayProgressValue ? ( + reverseAnimation()}> + + + ) : null} + + Content goes here + + + ); +}; +``` + +This is the result when we play the animation with Reduce Motion off and on: + +| Reduce Motion: **off** | Reduce Motion: **on** | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| | | | + +When reduce Motion is off, the animation is played as specified; while when is **on** any [motion](../utils/isMotionAnimation.md) animation is played instantaneity (using a duration of 0), while other properties, like opacity, are played as specified. + +## Sequential animation + +Let's consider the following animation: + + + +
+The animation is defined as: + +```tsx +const { + play: play2, + animatedStyle: animatedStyle2, + progress: progress2, +} = useAccessibleAnimation({ + duration: 300, + useNativeDriver: false, + from: { + opacity: 0, + width: 0, + left: MAX_LINE_WIDTH / 2, + }, + to: { + opacity: 1, + width: MAX_LINE_WIDTH, + left: theme.padding.big, + }, +}); +const { play: play3, animatedStyle: animatedStyle3 } = useAccessibleAnimation({ + duration: 300, + useNativeDriver: false, + from: { + height: 2, + marginTop: -1, + }, + to: { + height: 200, + marginTop: -100, + }, +}); + +const playAnimation = () => { + play2().start(() => { + play3().start(); + }); +}; +``` + +It's a two-part animation, where the first one the animates the view width and opacity: + +```js +from: { + opacity: 0, + width: 0, + left: MAX_LINE_WIDTH / 2, +}, +to: { + opacity: 1, + width: MAX_LINE_WIDTH, + left: theme.padding.big, +} +``` + +The second one the height: + +```js +from: { + height: 2, + marginTop: -1, +}, +to: { + height: 200, + marginTop: -100, +}, +``` + +Let's play the animation when **reduce motion** is enabled: + + + +
+ +The animation doesn't look right. The first animation is played correctly, but: + +- the width animation is played with a duration of 0s +- the fade animation is played with a duration of 300ms + +After that, the height jumps instantly from **2** to **200**. + +### skipIfReduceMotionEnabled + +One way to fix the animation is using the `skipIfReduceMotionEnabled` parameter; as this makes all the animations defined to be played instantly: + +```ts +const { + play: play2, + animatedStyle: animatedStyle2, + progress: progress2, +} = useAccessibleAnimation({ + duration: 300, + useNativeDriver: false, + skipIfReduceMotionEnabled: true, + from: { + opacity: 0, + width: 0, + left: MAX_LINE_WIDTH / 2, + }, + to: { + opacity: 1, + width: MAX_LINE_WIDTH, + left: theme.padding.big, + }, +}); +``` + +The result is: + + + +## Related guidelines + +- [Animations](../guidelines/animations) diff --git a/packages/animations/docs/hooks/useAnimationDuration.md b/packages/animations/docs/hooks/useAnimationDuration.md new file mode 100644 index 00000000..5100e677 --- /dev/null +++ b/packages/animations/docs/hooks/useAnimationDuration.md @@ -0,0 +1,55 @@ +# useAnimationDuration + +When passing a motion property, returns 0 if [Reduce Motion](../hooks/useAMAContext#isreducemotionenabled) is enabled otherwise the given value. + + +## Usage + +```js +import { useAnimationDuration } from 'react-native-ama'; + +const { getAnimationDuration } = useAccessibleAnimationDuration(); +``` + +## getAnimationDuration + +### Syntax + +```js +function getAnimationDuration( + property: ViewStyle, + durationMS: number, +): WithTimingConfig {} +``` + +| Property | Description | +| ---------- | -------------------------------------------- | +| property | The property to animate | +| durationMS | The duration to use if reduced motion is off | + +### Example + +We can create more accessible animations when using Reanimated: + +```ts +const value = useSharedValue(0); + +const animatedStyles = useAnimatedStyle(() => { + return { + transform: [{ translateX: value.value * 255 }], + }; +}); + +const playAnimation = () => { + value.value = withTiming( + Math.random(), + getAnimationDuration('translateX', 300), + ); +}; +``` + +Because we specified `translateX` as the property we're going to use for the animation, and considering that that property is a [motion animation](../utils/isMotionAnimation.md); `playAnimation` will use a duration of **300ms** when reduce motion is _off_, and duration of **0s** when is on + +## Related guidelines + +- [Animations](../guidelines/animations) diff --git a/packages/animations/docs/hooks/useReanimatedTiming.md b/packages/animations/docs/hooks/useReanimatedTiming.md new file mode 100644 index 00000000..4b7181be --- /dev/null +++ b/packages/animations/docs/hooks/useReanimatedTiming.md @@ -0,0 +1,164 @@ +# useReanimatedTiming + +This hooks allow using custom [withTiming](#withtiming) and `withSpring` functions that are reduce motion aware. + +## Usage + +```js +import { useReanimatedTiming } from 'react-native-ama'; + +const { withTiming, withSrping } = useReanimatedTiming(); +``` + +## Example + +```tsx +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { + ClickableSpan, + Span, + isMotionAnimation, + useAMAContext, + useAnimationDuration, + useReanimatedTiming, +} from 'react-native-ama'; +import Animated, { + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; + +export const ReanimatedReduceMotionScreen = () => { + const value = useSharedValue(0); + const { withTiming, withSpring } = useReanimatedTiming(); + + const animatedStyles = useAnimatedStyle(() => { + return { + transform: [{ translateX: value.value * 255 }], + }; + }); + + const testWithTiming = () => { + value.value = withTiming('translateX', Math.random(), { duration: 300 }); + }; + + const testWithSpring = () => { + value.value = withSpring('translateX', Math.random()); + }; + + return ( + + + This example shows how to use the{' '} + {}}>getAnimationDuration{' '} + with Reanimated for a more accessible animations. + + + + + withTiming + + + withSpring + + + ); +}; + +const styles = StyleSheet.create({ + view: { + paddingHorizontal: theme.padding.big, + }, + box: { + width: 100, + height: 100, + borderRadius: 20, + backgroundColor: theme.color.mixed, + }, + intro: { + lineHeight: theme.lineHeight.medium, + }, +}); +``` + +## withTiming + +Under the hood calls the +reanimated [withTiming](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming) function. + +If the given `propertyKey` is a motion one and [reduce motion](/core/hooks/useAMAContext#isreducemotionenabled) is enabled, the +force the duration to be 0, before calling `withTiming`. + +### Syntax + +```js +withTiming( + propertyKey +: +keyof +ViewStyle, + toValue +: +number, + config +: +WithTimingConfig = {}, + callback ? : AnimationCallback +) +; +``` + +| Property | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| propertyKey | The animation key that will be used with the `useSharedValue` | +| toValue | The [target value](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming#tovalue-number--string) parameter passed to the original `withTiming` | +| config | The [config](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming#options-object) parameter passed to the original `withTiming` | +| callback | The [callback](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming#callback-functionoptional) parameter passed to the original `withTiming` | + +#### Example + +```js +value.value = withTiming('translateX', Math.random(), { duration: 300 }); +``` + +## withSpring + +Under the hood calls the +reanimated [withSpring](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withSpring) function. + +If the given `propertyKey` is a motion one and [reduce motion](/core/hooks/useAMAContext#isreducemotionenabled) is enabled, +then calls `withTiming` function with duration 0 instead. + +### Syntax + +```js +withTiming( + propertyKey +: +keyof +ViewStyle, + toValue +: +number, + config ? : WithSpringConfig, + callback ? : AnimationCallback, +) +; +``` + +| Property | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| propertyKey | The animation key that will be used with the `useSharedValue` | +| toValue | The [target value](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming#tovalue-number--string) parameter passed to the original `withTiming` | +| config | The [config](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming#options-object) parameter passed to the original `withTiming` | +| callback | The [callback](https://docs.swmansion.com/react-native-reanimated/docs/api/animations/withTiming#callback-functionoptional) parameter passed to the original `withTiming` | + +#### Example + +```js +value.value = withSpring('translateX', Math.random()); +``` + +## Related guidelines + +- [Animations](../guidelines/animations) diff --git a/packages/animations/docs/utilities/_category_.json b/packages/animations/docs/utilities/_category_.json new file mode 100644 index 00000000..bd5207f7 --- /dev/null +++ b/packages/animations/docs/utilities/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Utilities", + "collapsible": true, + "collapsed": false +} diff --git a/packages/animations/docs/utilities/isMotionAnimation.md b/packages/animations/docs/utilities/isMotionAnimation.md new file mode 100644 index 00000000..ef8921a1 --- /dev/null +++ b/packages/animations/docs/utilities/isMotionAnimation.md @@ -0,0 +1,72 @@ +# isMotionAnimation + +`isMotionAnimation` can be used to check if a style key is considered a motion animation, i.e.: `translateX`, `left`, `right`, etc... + +## Syntax + +```js +function isMotionAnimation(property: ViewStyle): boolean {} +``` + +| Property | Description | +| -------- | --------------------- | +| property | The property to check | + +## Example + +```tsx +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { + isMotionAnimation, + useAMAContext, + useAccessibleAnimationDuration, +} from 'react-native-ama'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, + withTiming, +} from 'react-native-reanimated'; + +export const ReanimatedReduceMotionScreen = () => { + const { getAnimationDuration } = useAccessibleAnimationDuration(); + const { isReduceMotionEnabled } = useAMAContext(); + const value = useSharedValue(0); + + const animatedStyles = useAnimatedStyle(() => { + return { + transform: [{ translateX: value.value * 255 }], + }; + }); + + const testWithSpring = () => { + const to = Math.random(); + + value.value = + isReduceMotionEnabled && isMotionAnimation('translateX') + ? withTiming(to, { duration: 0 }) + : withSpring(to); + }; + + return ( + + + + + + ); +}; + +const styles = StyleSheet.create({ + view: { + paddingHorizontal: 24, + }, + box: { + width: 100, + height: 100, + borderRadius: 20, + backgroundColor: 'red', + }, +}); +```