Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
title: Migration from Paper 5.x to 6.x
---

TBC

## General changes

### Animations

React Native Paper 6 uses [Reanimated](https://docs.swmansion.com/react-native-reanimated/) for animations as opposed to the built-in React Native `Animated` API.
React Native Paper 6 uses [Reanimated](https://docs.swmansion.com/react-native-reanimated/) for most animations instead of the built-in React Native `Animated` API. So make sure to install `react-native-reanimated` 4.3.0 or later and `react-native-worklets` 0.8.1 or later, then complete the Reanimated setup. See the [getting started guide](./getting-started) for Expo and Community CLI instructions.

The following props now accept animated styles returned from `useAnimatedStyle`. They no longer accept `Animated.Value` or `Animated.AnimatedInterpolation` where these were previously supported:

Expand All @@ -19,6 +17,7 @@ The following props now accept animated styles returned from `useAnimatedStyle`.
- `Card`: `style`
- `Chip`: `style`
- `Dialog`: `style`
- `Drawer.CollapsedItem`: `style`
Comment thread
satya164 marked this conversation as resolved.
- `FAB` and `FAB.Extended`: `style`
- `IconButton`: `style`
- `Menu`: `contentStyle`
Expand Down
14 changes: 14 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ export default defineConfig(
},
],

'no-restricted-imports': [
'error',
{
paths: [
{
name: 'react-native',
importNames: ['Animated', 'PanResponder'],
message:
'Use react-native-reanimated for animations and react-native-gesture-handler for gestures.',
},
],
},
],

'@react-native/platform-colors': 'error',

'promise/no-callback-in-promise': 'error',
Expand Down
1 change: 1 addition & 0 deletions example/src/Examples/ProgressBarExample.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
// eslint-disable-next-line no-restricted-imports -- TODO: remove after ProgressBar migrates to Reanimated.
import { View, StyleSheet, Animated } from 'react-native';

import {
Expand Down
1 change: 1 addition & 0 deletions src/components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
// eslint-disable-next-line no-restricted-imports -- TODO: migrate ActivityIndicator to Reanimated.
import { Animated, Easing, Platform, StyleSheet, View } from 'react-native';
import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native';

Expand Down
2 changes: 1 addition & 1 deletion src/components/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const LARGE_SIZE = 16;
const MAX_LARGE_WIDTH = 36;
const LARGE_PADDING = 4;

export type Props = TextProps & {
export type Props = Omit<TextProps, 'style'> & {
/**
* Whether the badge is visible
*/
Expand Down
1 change: 1 addition & 0 deletions src/components/BottomNavigation/BottomNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
// eslint-disable-next-line no-restricted-imports -- TODO: migrate BottomNavigation to Reanimated.
import { Animated, Platform, StyleSheet, View } from 'react-native';
import type {
ColorValue,
Expand Down
1 change: 1 addition & 0 deletions src/components/BottomNavigation/BottomNavigationBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
// eslint-disable-next-line no-restricted-imports -- TODO: migrate BottomNavigation to Reanimated.
import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native';
import type {
ColorValue,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import type { ReactNode } from 'react';
// eslint-disable-next-line no-restricted-imports -- TODO: migrate BottomNavigation to Reanimated.
import { Animated, Platform, View } from 'react-native';
import type { ViewProps } from 'react-native';

Expand Down
204 changes: 66 additions & 138 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ import type {
ViewStyle,
} from 'react-native';

import Animated, {
Easing,
ReduceMotion,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import Animated, { cubicBezier, type CSSStyle } from 'react-native-reanimated';

import { CheckboxTokens } from './tokens';
import { getSelectionVisualState } from './utils';
Expand Down Expand Up @@ -123,7 +117,9 @@ const Checkbox = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);

const reduceMotion = useReduceMotion();

const { direction } = useLocale();
// Web (react-native-web) doesn't auto-mirror layout, so flip the mask
// anchor manually for RTL. Native handles it via `I18nManager`.
Expand All @@ -132,56 +128,6 @@ const Checkbox = ({

const selected = status === 'checked' || status === 'indeterminate';

const reanimatedReduceMotion = reduceMotion
? ReduceMotion.Always
: ReduceMotion.Never;

const fillTimingConfig = React.useMemo(
() => ({
duration: theme.motion.duration.short2,
easing: Easing.bezier(...theme.motion.easing.standard),
reduceMotion: reanimatedReduceMotion,
}),
[
theme.motion.duration.short2,
theme.motion.easing.standard,
reanimatedReduceMotion,
]
);
const checkTimingConfig = React.useMemo(
() => ({
duration: theme.motion.duration.short3,
easing: Easing.bezier(...theme.motion.easing.standard),
reduceMotion: reanimatedReduceMotion,
}),
[
theme.motion.duration.short3,
theme.motion.easing.standard,
reanimatedReduceMotion,
]
);

// 0 = unselected (outline only), 1 = selected (filled + drawn icon).
const fillProgress = useSharedValue(selected ? 1 : 0);
const checkProgress = useSharedValue(selected ? 1 : 0);
const firstRender = React.useRef(true);

React.useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
const target = selected ? 1 : 0;
fillProgress.value = withTiming(target, fillTimingConfig);
checkProgress.value = withTiming(target, checkTimingConfig);
}, [
selected,
fillProgress,
checkProgress,
fillTimingConfig,
checkTimingConfig,
]);

// Visual state (colors + opacity) for the static layers. `hovered` /
// `pressed` aren't tracked here — `TouchableRipple` owns the press ripple
// and hover overlay.
Expand All @@ -194,19 +140,45 @@ const Checkbox = ({
customUncheckedColor: uncheckedColor,
});

// Outline fades out as fill fades in (and vice versa).
const outlineAnimatedStyle = useAnimatedStyle(() => ({
opacity: 1 - fillProgress.value,
}));
const fillTransitionTimingFunction = cubicBezier(
...theme.motion.easing.standard
);

const fillTransitionDuration = reduceMotion
? 0
: theme.motion.duration.short2;

const checkTransitionDuration = reduceMotion
? 0
: theme.motion.duration.short3;

const checkTransitionTimingFunction = cubicBezier(
...theme.motion.easing.standard
);

const fillAnimatedStyle = useAnimatedStyle(() => ({
opacity: fillProgress.value,
}));
const outlineStyle: CSSStyle<ViewStyle> = {
borderColor: visual.outlineColor,
opacity: selected ? 0 : 1,
transitionDuration: fillTransitionDuration,
transitionProperty: ['opacity'],
transitionTimingFunction: fillTransitionTimingFunction,
Comment thread
satya164 marked this conversation as resolved.
};

const maskAnimatedStyle = useAnimatedStyle(() => ({
width: checkProgress.value * CONTAINER_SIZE,
opacity: checkProgress.value,
}));
const fillStyle: CSSStyle<ViewStyle> = {
backgroundColor: visual.containerColor,
opacity: selected ? 1 : 0,
transitionDuration: fillTransitionDuration,
transitionProperty: ['opacity'],
transitionTimingFunction: fillTransitionTimingFunction,
};

const maskStyle: CSSStyle<ViewStyle> = {
width: selected ? CONTAINER_SIZE : 0,
opacity: selected ? 1 : 0,
transitionDuration: checkTransitionDuration,
transitionProperty: ['width', 'opacity'],
transitionTimingFunction: checkTransitionTimingFunction,
};

// Remember the last drawn glyph so the reveal-mask can finish collapsing
// when `selected` flips back to false. Computed via the "derive state
Expand Down Expand Up @@ -283,92 +255,48 @@ const Checkbox = ({
<View style={[styles.container, { opacity: visual.containerOpacity }]}>
<Animated.View
pointerEvents="none"
style={[
styles.outline,
{ borderColor: visual.outlineColor },
outlineAnimatedStyle,
]}
style={[styles.outline, outlineStyle]}
/>
<Animated.View
pointerEvents="none"
style={[styles.fill, fillStyle]}
/>
<Animated.View
style={[
styles.fill,
{ backgroundColor: visual.containerColor },
fillAnimatedStyle,
flipMaskForWebRTL
? styles.checkmarkMaskWebRTL
: styles.checkmarkMask,
maskStyle,
]}
/>
{showIndeterminate ? (
<Animated.View
style={[
flipMaskForWebRTL
? styles.checkmarkMaskWebRTL
: styles.checkmarkMask,
maskAnimatedStyle,
]}
>
>
{showIndeterminate ? (
<View style={styles.checkmarkContent}>
<View
style={[styles.dash, { backgroundColor: visual.iconColor }]}
/>
</View>
</Animated.View>
) : (
<Checkmark
color={visual.iconColor}
maskAnimatedStyle={maskAnimatedStyle}
flipMaskForWebRTL={flipMaskForWebRTL}
isRTL={direction === 'rtl'}
autoSwapsBorders={Platform.OS !== 'web'}
/>
)}
) : (
<View style={styles.checkmarkContent}>
<View
style={[
styles.checkmarkGlyph,
{ borderColor: visual.iconColor },
// Native platforms auto-swap border sides in RTL, so
// pre-swap them to preserve the checkmark orientation.
direction === 'rtl' && Platform.OS !== 'web'
? styles.checkmarkGlyphRTL
: null,
]}
/>
</View>
)}
</Animated.View>
</View>
</View>
</TouchableRipple>
);
};

/**
* Reveal-mask checkmark: an L-shape (borderLeftWidth + borderBottomWidth
* rotated -45deg) inside a directional-anchored View whose width animates
* 0 → CONTAINER_SIZE so the stroke "draws in" along the writing direction.
*/
const Checkmark = ({
color,
maskAnimatedStyle,
flipMaskForWebRTL,
isRTL,
autoSwapsBorders,
}: {
color: ColorValue;
maskAnimatedStyle: ReturnType<typeof useAnimatedStyle>;
flipMaskForWebRTL: boolean;
isRTL: boolean;
// True on platforms whose layout engine auto-swaps `borderLeftWidth`
// ↔ `borderRightWidth` in RTL (Android, iOS); false on web (RNW).
autoSwapsBorders: boolean;
}) => {
return (
<Animated.View
style={[
flipMaskForWebRTL ? styles.checkmarkMaskWebRTL : styles.checkmarkMask,
maskAnimatedStyle,
]}
>
<View style={styles.checkmarkContent}>
<View
style={[
styles.checkmarkGlyph,
{ borderColor: color },
// On platforms that auto-swap borders in RTL, pre-swap them
// here so the swap brings them back to the LTR orientation.
isRTL && autoSwapsBorders ? styles.checkmarkGlyphRTL : null,
]}
/>
</View>
</Animated.View>
);
};

// Web-only style; not in StyleSheet because `outline` is outside ViewStyle.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const webNoOutline = { outline: 'none' } as unknown as ViewStyle;
Expand Down
4 changes: 0 additions & 4 deletions src/components/Checkbox/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,9 @@ const getOutlineColor = ({

const getIconColor = ({
theme,
selected,
disabled,
error,
}: SelectionState): ColorValue => {
if (!selected) {
return 'transparent';
}
if (disabled) {
return theme.colors[CheckboxTokens.disabledIconColor];
}
Expand Down
Loading