+
Surface
);
@@ -35,9 +35,9 @@ export default MyComponent;
const styles = StyleSheet.create({
surface: {
- padding: 8,
height: 80,
width: 80,
+ padding: 8,
alignItems: 'center',
justifyContent: 'center',
},
@@ -52,11 +52,131 @@ const styles = StyleSheet.create({
-### children (required)
+### backgroundColor
-
+
+
+
+
+### borderRadius
+
+
+
+
+
+
+
+### borderBottomEndRadius
+
+
+
+
+
+
+
+### borderBottomLeftRadius
+
+
+
+
+
+
+
+### borderBottomRightRadius
+
+
+
+
+
+
+
+### borderBottomStartRadius
+
+
+
+
+
+
+
+### borderEndEndRadius
+
+
+
+
+
+
+
+### borderEndStartRadius
+
+
+
+
+
+
+
+### borderStartEndRadius
+
+
+
+
+
+
+
+### borderStartStartRadius
+
+
+
+
+
+
+
+### borderTopEndRadius
+
+
+
+
+
+
+
+### borderTopLeftRadius
+
+
+
+
+
+
+
+### borderTopRightRadius
+
+
+
+
+
+
+
+### borderTopStartRadius
+
+
+
+
+
+
+
+### borderCurve
+
+
+
+
+
+
+
+### transitionDuration
+
+
+
+
@@ -92,6 +212,14 @@ const styles = StyleSheet.create({
+### children (required)
+
+
+
+
+
+
+
### testID
diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md
index 1fcd32bd25..e488b101ef 100644
--- a/docs/6.x/docs/guides/migration.md
+++ b/docs/6.x/docs/guides/migration.md
@@ -4,8 +4,162 @@ 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.
+
+The following props now accept animated styles returned from `useAnimatedStyle`. They no longer accept `Animated.Value` or `Animated.AnimatedInterpolation` where these were previously supported:
+
+- `Appbar.Action` and `Appbar.BackAction`: `style`
+- `Badge`: `style`
+- `Banner`: `style`
+- `Button`: `style`
+- `Card`: `style`
+- `Chip`: `style`
+- `Dialog`: `style`
+- `FAB` and `FAB.Extended`: `style`
+- `IconButton`: `style`
+- `Menu`: `contentStyle`
+- `Modal`: `contentContainerStyle`
+- `Searchbar`: `style`
+- `Snackbar`: `style`
+- `Surface`: `style`
+- `ToggleButton`: `style`
+
+So you can use Reanimated's `useSharedValue` and `useAnimatedStyle` to animate these components instead of the React Native `Animated` API.
+
+```tsx
+import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
+
+const MyComponent = () => {
+ const opacity = useSharedValue(1);
+ const animatedStyle = useAnimatedStyle(() => ({
+ opacity: opacity.value,
+ }));
+
+ return
Button ;
+};
+```
+
+### Elevation
+
+The `elevation` prop no longer accepts a React Native `Animated.Value` in the following components:
+
+- `Banner`
+- `Card`
+- `Searchbar`
+- `Snackbar`
+- `Surface`
+
+You can use an elevation level from `0` to `5` instead. Changes to the elevation level are animated automatically.
+
+### Styles
+
+The following component style props no longer support overriding their background color or border radius:
+
+- `Banner`
+- `Button`
+- `Card`
+- `Chip`
+- `Dialog`
+- `Menu`: `contentStyle`
+- `Searchbar`
+- `Snackbar`
+
+You can use the component's color prop where available, or override the corresponding theme colors.
+
+### Test IDs
+
+Some hardcoded and generated test IDs have been removed for the following components:
+
+- `Appbar.Header`: `${testID}-root-layer`
+- `Surface`: `surface` and `${testID}-outer-layer`
+
+You can specify a `testID` explicitly and use that value to query the component.
+
## Components
+### Appbar
+
+The `style` props for `Appbar` and `Appbar.Header` no longer accept `Animated.Value` or `Animated.AnimatedInterpolation`. They only accept static styles.
+
+The `style.elevation` property is no longer supported. Use the `elevated` prop to control Appbar elevation.
+
+### Surface
+
+- The `elevation` prop no longer accepts a React Native `Animated.Value`. Any `elevation` changes are animated automatically.
+- The `style` prop no longer configures elevation, background color, or border radius. Use these props instead:
+ - `elevation`
+ - `backgroundColor`
+ - `borderRadius`
+ - `borderBottomEndRadius`
+ - `borderBottomLeftRadius`
+ - `borderBottomRightRadius`
+ - `borderBottomStartRadius`
+ - `borderEndEndRadius`
+ - `borderEndStartRadius`
+ - `borderStartEndRadius`
+ - `borderStartStartRadius`
+ - `borderTopEndRadius`
+ - `borderTopLeftRadius`
+ - `borderTopRightRadius`
+ - `borderTopStartRadius`
+ - `borderCurve`
+- The `pointerEvents` prop is no longer supported as it's deprecated in React Native Web. You can specify `pointerEvents` in the `style` prop instead.
+- The `overflow: 'hidden'` style is no longer supported in `style` as it can clip shadows. You can nest a `View` inside the `Surface` and apply `overflow: 'hidden'` to that instead.
+- The default `testID` for `Surface` was removed. You can specify a `testID` explicitly if you need it.
+
+e.g.:
+
+```diff
+
++
+ Content
++
+
+```
+
+### Modal
+
+- The `contentContainerStyle` prop no longer configures the background color or any border radius property. We have added new props for these:
+ - `contentBackgroundColor`
+ - `contentBorderRadius`
+- We have added the `contentElevation` prop to configure the elevation of the modal content.
+
+e.g.:
+
+```diff
+
+ Content
+
+```
+
+### Dialog
+
+- The default elevation changed from level `1` to level `3`.
+- The `style` prop no longer configures the background color or border radius. You can override `theme.colors.surfaceContainerHigh` and `theme.shapes.corner.extraLarge` using the `theme` prop instead.
+
### TextInput
The Paper 6.x `TextInput` is a complete rewrite with a new API. Import the component the same way, but note that the props and behavior have changed significantly.
diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json
index 300c315b99..b223c9c0a8 100644
--- a/docs/src/data/componentDocs6x.json
+++ b/docs/src/data/componentDocs6x.json
@@ -153,7 +153,11 @@
"tsType": {
"name": "boolean"
},
- "description": "@supported Available in v5.x with theme version 3\nWhether Appbar background should have the elevation along with primary color pigment."
+ "description": "@supported Available in v5.x with theme version 3\nWhether Appbar background should have the elevation along with primary color pigment.",
+ "defaultValue": {
+ "value": "false",
+ "computed": false
+ }
},
"safeAreaInsets": {
"required": false,
@@ -206,19 +210,23 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "Omit",
"elements": [
{
"name": "ViewStyle"
+ },
+ {
+ "name": "literal",
+ "value": "'elevation'"
}
],
- "raw": "StyleProp
"
+ "raw": "Omit"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
}
@@ -304,19 +312,19 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
"name": "ViewStyle"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp>"
},
"description": ""
},
@@ -416,19 +424,19 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
"name": "ViewStyle"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp>"
},
"description": ""
},
@@ -694,19 +702,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "AppbarStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -1032,10 +1034,16 @@
"name": "StyleProp",
"elements": [
{
- "name": "TextStyle"
+ "name": "AnimatedStyle",
+ "elements": [
+ {
+ "name": "TextStyle"
+ }
+ ],
+ "raw": "AnimatedStyle"
}
],
- "raw": "StyleProp"
+ "raw": "StyleProp>"
},
"description": ""
},
@@ -1147,37 +1155,7 @@
"elevation": {
"required": false,
"tsType": {
- "name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
- "elements": [
- {
- "name": "literal",
- "value": "0"
- },
- {
- "name": "literal",
- "value": "1"
- },
- {
- "name": "literal",
- "value": "2"
- },
- {
- "name": "literal",
- "value": "3"
- },
- {
- "name": "literal",
- "value": "4"
- },
- {
- "name": "literal",
- "value": "5"
- },
- {
- "name": "Animated.Value"
- }
- ]
+ "name": "Elevation"
},
"description": "@supported Available in v5.x with theme version 3\nChanges Banner shadow and background on iOS and Android.",
"defaultValue": {
@@ -1195,19 +1173,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -1234,7 +1206,35 @@
"onShowAnimationFinished": {
"required": false,
"tsType": {
- "name": "Animated.EndCallback"
+ "name": "signature",
+ "type": "function",
+ "raw": "(result: { finished: boolean }) => void",
+ "signature": {
+ "arguments": [
+ {
+ "name": "result",
+ "type": {
+ "name": "signature",
+ "type": "object",
+ "raw": "{ finished: boolean }",
+ "signature": {
+ "properties": [
+ {
+ "key": "finished",
+ "value": {
+ "name": "boolean",
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "return": {
+ "name": "void"
+ }
+ }
},
"description": "\nOptional callback that will be called after the opening animation finished running normally",
"defaultValue": {
@@ -1245,7 +1245,35 @@
"onHideAnimationFinished": {
"required": false,
"tsType": {
- "name": "Animated.EndCallback"
+ "name": "signature",
+ "type": "function",
+ "raw": "(result: { finished: boolean }) => void",
+ "signature": {
+ "arguments": [
+ {
+ "name": "result",
+ "type": {
+ "name": "signature",
+ "type": "object",
+ "raw": "{ finished: boolean }",
+ "signature": {
+ "properties": [
+ {
+ "key": "finished",
+ "value": {
+ "name": "boolean",
+ "required": true
+ }
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "return": {
+ "name": "void"
+ }
+ }
},
"description": "\nOptional callback that will be called after the closing animation finished running normally",
"defaultValue": {
@@ -3226,19 +3254,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -3449,37 +3471,7 @@
"elevation": {
"required": false,
"tsType": {
- "name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
- "elements": [
- {
- "name": "literal",
- "value": "0"
- },
- {
- "name": "literal",
- "value": "1"
- },
- {
- "name": "literal",
- "value": "2"
- },
- {
- "name": "literal",
- "value": "3"
- },
- {
- "name": "literal",
- "value": "4"
- },
- {
- "name": "literal",
- "value": "5"
- },
- {
- "name": "Animated.Value"
- }
- ]
+ "name": "Elevation"
},
"description": "Changes Card shadow and background on iOS and Android.",
"defaultValue": {
@@ -3503,19 +3495,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -3543,6 +3529,19 @@
"name": "boolean"
},
"description": "Pass down accessible from card props to touchable"
+ },
+ "ref": {
+ "required": false,
+ "tsType": {
+ "name": "ReactRef",
+ "raw": "React.Ref",
+ "elements": [
+ {
+ "name": "View"
+ }
+ ]
+ },
+ "description": "Reference to the card container."
}
}
},
@@ -4590,19 +4589,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -4646,6 +4639,19 @@
},
"description": "Specifies the largest possible scale a text font can reach."
},
+ "ref": {
+ "required": false,
+ "tsType": {
+ "name": "ReactRef",
+ "raw": "React.Ref",
+ "elements": [
+ {
+ "name": "View"
+ }
+ ]
+ },
+ "description": "Reference to the chip container."
+ },
"role": {
"defaultValue": {
"value": "'button'",
@@ -5273,19 +5279,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -6124,10 +6124,16 @@
"name": "StyleProp",
"elements": [
{
- "name": "ViewStyle"
+ "name": "AnimatedStyle",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ }
+ ],
+ "raw": "AnimatedStyle"
}
],
- "raw": "StyleProp"
+ "raw": "StyleProp>"
},
"description": "Style for positioning the FAB. The visual treatment (size, shape, color)\nis driven by `variant` and `size`."
},
@@ -6339,10 +6345,16 @@
"name": "StyleProp",
"elements": [
{
- "name": "ViewStyle"
+ "name": "AnimatedStyle",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ }
+ ],
+ "raw": "AnimatedStyle"
}
],
- "raw": "StyleProp"
+ "raw": "StyleProp>"
},
"description": "Style for positioning the FAB. The visual treatment (size, shape, color)\nis driven by `variant` and `size`."
},
@@ -7929,19 +7941,19 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
"name": "ViewStyle"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp>"
},
"description": ""
},
@@ -9059,19 +9071,13 @@
"contentStyle": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": "Style of menu's inner content."
},
@@ -9375,10 +9381,10 @@
"Modal": {
"filepath": "Modal.tsx",
"title": "Modal",
- "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n const containerStyle = { backgroundColor: 'white', padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss. \n \n \n \n Show\n \n \n );\n};\n\nexport default MyComponent;\n```",
+ "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n\n const containerStyle = { padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss. \n \n \n \n Show\n \n \n );\n};\n\nexport default MyComponent;\n```",
"link": "modal",
"data": {
- "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n const containerStyle = { backgroundColor: 'white', padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss. \n \n \n \n Show\n \n \n );\n};\n\nexport default MyComponent;\n```",
+ "description": "The Modal component is a simple way to present content above an enclosing view.\nTo render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.\nNote that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Modal, Portal, Text, Button, PaperProvider } from 'react-native-paper';\n\nconst MyComponent = () => {\n const [visible, setVisible] = React.useState(false);\n\n const showModal = () => setVisible(true);\n const hideModal = () => setVisible(false);\n\n const containerStyle = { padding: 20 };\n\n return (\n \n \n \n Example Modal. Click outside this area to dismiss. \n \n \n \n Show\n \n \n );\n};\n\nexport default MyComponent;\n```",
"displayName": "Modal",
"methods": [],
"statics": [],
@@ -9457,21 +9463,42 @@
"contentContainerStyle": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
- "description": "Style for the content of the modal"
+ "description": "Style for the content of the modal.\n\nBackground color and border radius should be specified via props instead:\n- `contentBackgroundColor`\n- `contentBorderRadius`"
+ },
+ "contentBackgroundColor": {
+ "required": false,
+ "tsType": {
+ "name": "SurfaceProps['backgroundColor']",
+ "raw": "SurfaceProps['backgroundColor']"
+ },
+ "description": "Background color of the modal content. Defaults to transparent.",
+ "defaultValue": {
+ "value": "'transparent'",
+ "computed": false
+ }
+ },
+ "contentBorderRadius": {
+ "required": false,
+ "tsType": {
+ "name": "SurfaceProps['borderRadius']",
+ "raw": "SurfaceProps['borderRadius']"
+ },
+ "description": "Border radius of the modal content."
+ },
+ "contentElevation": {
+ "required": false,
+ "tsType": {
+ "name": "Elevation"
+ },
+ "description": "Elevation level of the modal content. Defaults to level 1."
},
"style": {
"required": false,
@@ -10546,37 +10573,7 @@
"elevation": {
"required": false,
"tsType": {
- "name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
- "elements": [
- {
- "name": "literal",
- "value": "0"
- },
- {
- "name": "literal",
- "value": "1"
- },
- {
- "name": "literal",
- "value": "2"
- },
- {
- "name": "literal",
- "value": "3"
- },
- {
- "name": "literal",
- "value": "4"
- },
- {
- "name": "literal",
- "value": "5"
- },
- {
- "name": "Animated.Value"
- }
- ]
+ "name": "Elevation"
},
"description": "@supported Available in v5.x with theme version 3\nChanges Searchbar shadow and background on iOS and Android.",
"defaultValue": {
@@ -10600,19 +10597,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -11018,37 +11009,7 @@
"elevation": {
"required": false,
"tsType": {
- "name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
- "elements": [
- {
- "name": "literal",
- "value": "0"
- },
- {
- "name": "literal",
- "value": "1"
- },
- {
- "name": "literal",
- "value": "2"
- },
- {
- "name": "literal",
- "value": "3"
- },
- {
- "name": "literal",
- "value": "4"
- },
- {
- "name": "literal",
- "value": "5"
- },
- {
- "name": "Animated.Value"
- }
- ]
+ "name": "Elevation"
},
"description": "@supported Available in v5.x with theme version 3\nChanges Snackbar shadow and background on iOS and Android.",
"defaultValue": {
@@ -11092,19 +11053,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "SurfaceStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -11145,54 +11100,186 @@
"Surface": {
"filepath": "Surface.tsx",
"title": "Surface",
- "description": "Surface is a basic container that can give depth to an element with elevation shadow.\nOn dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.\nSee [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.\nOverlay and shadow can be applied by specifying the `elevation` property both on Android and iOS.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Surface, Text } from 'react-native-paper';\nimport { StyleSheet } from 'react-native';\n\nconst MyComponent = () => (\n \n Surface \n \n);\n\nexport default MyComponent;\n\nconst styles = StyleSheet.create({\n surface: {\n padding: 8,\n height: 80,\n width: 80,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n```",
+ "description": "Surface is a basic container that can give depth to an element with elevation shadow.\n\nOn Android, Surface uses the native `elevation` style,\nand falls back to shadows that approximate the elevation on other platforms.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Surface, Text } from 'react-native-paper';\nimport { StyleSheet } from 'react-native';\n\nconst MyComponent = () => (\n \n Surface \n \n);\n\nexport default MyComponent;\n\nconst styles = StyleSheet.create({\n surface: {\n height: 80,\n width: 80,\n padding: 8,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n```",
"link": "surface",
"data": {
- "description": "Surface is a basic container that can give depth to an element with elevation shadow.\nOn dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.\nSee [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.\nOverlay and shadow can be applied by specifying the `elevation` property both on Android and iOS.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Surface, Text } from 'react-native-paper';\nimport { StyleSheet } from 'react-native';\n\nconst MyComponent = () => (\n \n Surface \n \n);\n\nexport default MyComponent;\n\nconst styles = StyleSheet.create({\n surface: {\n padding: 8,\n height: 80,\n width: 80,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n```",
+ "description": "Surface is a basic container that can give depth to an element with elevation shadow.\n\nOn Android, Surface uses the native `elevation` style,\nand falls back to shadows that approximate the elevation on other platforms.\n\n## Usage\n```js\nimport * as React from 'react';\nimport { Surface, Text } from 'react-native-paper';\nimport { StyleSheet } from 'react-native';\n\nconst MyComponent = () => (\n \n Surface \n \n);\n\nexport default MyComponent;\n\nconst styles = StyleSheet.create({\n surface: {\n height: 80,\n width: 80,\n padding: 8,\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n```",
"displayName": "Surface",
"methods": [],
"statics": [],
"props": {
- "children": {
- "required": true,
+ "backgroundColor": {
+ "required": false,
"tsType": {
- "name": "ReactReactNode",
- "raw": "React.ReactNode"
+ "name": "ColorValue"
},
- "description": "Content of the `Surface`."
+ "description": "Background color of the Surface. Overrides the color derived from\n`elevation`."
+ },
+ "borderRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of every corner of the Surface."
+ },
+ "borderBottomEndRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the bottom-end corner of the Surface."
+ },
+ "borderBottomLeftRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the bottom-left corner of the Surface."
+ },
+ "borderBottomRightRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the bottom-right corner of the Surface."
+ },
+ "borderBottomStartRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the bottom-start corner of the Surface."
+ },
+ "borderEndEndRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the end-end corner of the Surface."
+ },
+ "borderEndStartRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the end-start corner of the Surface."
+ },
+ "borderStartEndRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the start-end corner of the Surface."
+ },
+ "borderStartStartRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the start-start corner of the Surface."
+ },
+ "borderTopEndRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the top-end corner of the Surface."
+ },
+ "borderTopLeftRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the top-left corner of the Surface."
+ },
+ "borderTopRightRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the top-right corner of the Surface."
+ },
+ "borderTopStartRadius": {
+ "required": false,
+ "tsType": {
+ "name": "Extract['borderRadius']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
+ },
+ "description": "Radius of the top-start corner of the Surface."
+ },
+ "borderCurve": {
+ "required": false,
+ "tsType": {
+ "name": "ViewStyle['borderCurve']",
+ "raw": "ViewStyle['borderCurve']"
+ },
+ "description": "Corner curve of the Surface on iOS.",
+ "defaultValue": {
+ "value": "'continuous'",
+ "computed": false
+ }
+ },
+ "transitionDuration": {
+ "required": false,
+ "tsType": {
+ "name": "number"
+ },
+ "description": "Duration of the background, elevation, and shadow transitions in\nmilliseconds."
},
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
- "name": "ViewStyle"
+ "name": "Omit",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ },
+ {
+ "name": "union",
+ "raw": "keyof SurfaceVisualProps | 'elevation'",
+ "elements": [
+ {
+ "name": "unknown"
+ },
+ {
+ "name": "literal",
+ "value": "'elevation'"
+ }
+ ]
+ }
+ ],
+ "raw": "Omit"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle<\n Omit\n>"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
- "description": ""
+ "description": "Style of the Surface.\n\nThis doesn't support all View style properties:\n- Background color and border radius should be specified via props instead.\n- `overflow: 'hidden'` is not supported with `elevation` as it can clip the shadow.\n To achieve the same effect, wrap the content in a child View with the overflow style."
},
"elevation": {
"required": false,
"tsType": {
- "name": "union",
- "raw": "Elevation | Animated.Value",
- "elements": [
- {
- "name": "Elevation"
- },
- {
- "name": "Animated.Value"
- }
- ]
+ "name": "Elevation"
},
"description": "@supported Available in v5.x with theme version 3\nChanges shadows and background on iOS and Android.\nUsed to create UI hierarchy between components.\n\nNote: If `mode` is set to `flat`, Surface doesn't have a shadow.\n\nNote: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`.\nIt's no longer supported with theme version 3 and you should use `elevation` property instead.",
"defaultValue": {
@@ -11229,16 +11316,20 @@
},
"description": ""
},
+ "children": {
+ "required": true,
+ "tsType": {
+ "name": "ReactReactNode",
+ "raw": "React.ReactNode"
+ },
+ "description": "Content of the `Surface`."
+ },
"testID": {
"required": false,
"tsType": {
"name": "string"
},
- "description": "TestID used for testing purposes",
- "defaultValue": {
- "value": "'surface'",
- "computed": false
- }
+ "description": "TestID used for testing purposes"
},
"ref": {
"required": false,
@@ -11252,13 +11343,6 @@
]
},
"description": ""
- },
- "container": {
- "required": false,
- "tsType": {
- "name": "boolean"
- },
- "description": "@internal"
}
}
},
@@ -11780,19 +11864,19 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
"name": "ViewStyle"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp>"
},
"description": ""
},
diff --git a/example/src/Examples/BannerExample.tsx b/example/src/Examples/BannerExample.tsx
index 5679a92518..2a99b38866 100644
--- a/example/src/Examples/BannerExample.tsx
+++ b/example/src/Examples/BannerExample.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Dimensions, Image, Platform, StyleSheet, View } from 'react-native';
-import type { LayoutChangeEvent } from 'react-native';
import { Banner, FAB, Palette, useTheme } from 'react-native-paper';
@@ -15,13 +14,6 @@ const BannerExample = () => {
const [useCustomTheme, setUseCustomTheme] = React.useState(false);
const defaultTheme = useTheme();
- const [height, setHeight] = React.useState(0);
-
- const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {
- const { height: layoutHeight } = nativeEvent.layout;
- setHeight(layoutHeight);
- };
-
const customTheme = {
...defaultTheme,
colors: {
@@ -37,23 +29,7 @@ const BannerExample = () => {
return (
-
-
- {PHOTOS.map((uri) => (
-
-
-
- ))}
-
-
- setVisible(!visible)} />
{
console.log('Completed closing animation')
}
theme={useCustomTheme ? customTheme : defaultTheme}
- style={styles.banner}
>
Two line text string with two actions. One to two lines is preferable on
mobile.
+
+
+ {PHOTOS.map((uri) => (
+
+
+
+ ))}
+
+
+ setVisible(!visible)} />
);
};
@@ -117,12 +107,6 @@ const styles = StyleSheet.create({
},
},
}),
- banner: {
- position: 'absolute',
- top: 0,
- left: 0,
- width: '100%',
- },
photo: {
flex: 1,
},
diff --git a/example/src/Examples/ButtonExample.tsx b/example/src/Examples/ButtonExample.tsx
index e250644756..2a164e8e77 100644
--- a/example/src/Examples/ButtonExample.tsx
+++ b/example/src/Examples/ButtonExample.tsx
@@ -283,18 +283,22 @@ const ButtonExample = () => {
{}}
- style={styles.customRadius}
+ theme={{ shapes: { corner: { largeIncreased: 16 } } }}
>
Custom radius
- {}} style={styles.noRadius}>
+ {}}
+ theme={{ shapes: { corner: { largeIncreased: 0 } } }}
+ >
Without radius
{}}
- style={{ borderRadius: styles.customRadiusAndPadding.borderRadius }}
- contentStyle={styles.customRadiusAndPadding}
+ theme={{ shapes: { corner: { largeIncreased: 4 } } }}
+ contentStyle={styles.customPadding}
>
Custom radius and padding
@@ -380,17 +384,7 @@ const styles = StyleSheet.create({
width: '100%',
marginTop: 10,
},
- customRadius: {
- borderTopLeftRadius: 16,
- borderTopRightRadius: 0,
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 16,
- },
- noRadius: {
- borderRadius: 0,
- },
- customRadiusAndPadding: {
- borderRadius: 4,
+ customPadding: {
paddingHorizontal: 12,
paddingVertical: 6,
},
diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx
index 61c7ebe822..1061d4f904 100644
--- a/example/src/Examples/CardExample.tsx
+++ b/example/src/Examples/CardExample.tsx
@@ -104,17 +104,24 @@ const CardExample = () => {
/>
- {}}>
+ {}}
+ >
Share
- {}}>
+ {}}
+ >
Explore
{
accessibilityIgnoresInvertColors
/>
}
- style={[styles.chip, styles.customBorderRadius]}
+ style={styles.chip}
+ theme={{ shapes: { corner: { small: 16 } } }}
>
Compact with custom border radius
@@ -258,7 +259,8 @@ const ChipExample = () => {
accessibilityIgnoresInvertColors
/>
}
- style={[styles.chip, styles.customBorderRadius]}
+ style={styles.chip}
+ theme={{ shapes: { corner: { small: 16 } } }}
>
Compact with custom border radius
@@ -275,12 +277,15 @@ const ChipExample = () => {
{}}
- style={[
- styles.chip,
- {
- backgroundColor: color(customColor).alpha(0.2).rgb().string(),
+ style={styles.chip}
+ theme={{
+ colors: {
+ secondaryContainer: color(customColor)
+ .alpha(0.2)
+ .rgb()
+ .string(),
},
- ]}
+ }}
selectedColor={customColor}
>
Flat selected chip with custom color
@@ -296,12 +301,12 @@ const ChipExample = () => {
selected
mode="outlined"
onPress={() => {}}
- style={[
- styles.chip,
- {
- backgroundColor: color(customColor).alpha(0.2).rgb().string(),
+ style={styles.chip}
+ theme={{
+ colors: {
+ surface: color(customColor).alpha(0.2).rgb().string(),
},
- ]}
+ }}
selectedColor={customColor}
>
Outlined selected chip with custom color
@@ -403,9 +408,6 @@ const styles = StyleSheet.create({
marginVertical: 4,
marginHorizontal: 12,
},
- customBorderRadius: {
- borderRadius: 16,
- },
});
export default ChipExample;
diff --git a/example/src/Examples/Dialogs/DialogWithCustomColors.tsx b/example/src/Examples/Dialogs/DialogWithCustomColors.tsx
index f3625b835e..75b5ee6b6e 100644
--- a/example/src/Examples/Dialogs/DialogWithCustomColors.tsx
+++ b/example/src/Examples/Dialogs/DialogWithCustomColors.tsx
@@ -13,8 +13,10 @@ const DialogWithCustomColors = ({
diff --git a/example/src/Examples/SurfaceExample.tsx b/example/src/Examples/SurfaceExample.tsx
index fe420830ce..e941c7c843 100644
--- a/example/src/Examples/SurfaceExample.tsx
+++ b/example/src/Examples/SurfaceExample.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Animated, ScrollView, StyleSheet, View } from 'react-native';
+import { ScrollView, StyleSheet, View } from 'react-native';
import { Surface, Text, Palette, List, IconButton } from 'react-native-paper';
import type { Elevation } from 'react-native-paper';
@@ -12,19 +12,10 @@ const AnimatedSurface = () => {
const [index, setIndex] = React.useState(3);
const level = elevationLevels[index];
- const elevation = React.useRef(new Animated.Value(level)).current;
-
- React.useEffect(() => {
- Animated.timing(elevation, {
- toValue: level,
- duration: 250,
- useNativeDriver: false,
- }).start();
- }, [elevation, level]);
return (
-
+
{`Elevation ${level}`}
@@ -49,7 +40,13 @@ const SurfaceExample = () => {
const elevationValues: Elevation[] = [0, 1, 2, 3, 4, 5];
const renderSurface = (index: Elevation, mode: 'flat' | 'elevated') => (
-
+
{`Elevation ${index}`}
);
@@ -93,10 +90,14 @@ const SurfaceExample = () => {
-
+
Top
-
+
Bottom
@@ -121,7 +122,6 @@ const styles = StyleSheet.create({
surface: {
height: 120,
width: 120,
- borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
@@ -153,6 +153,8 @@ const styles = StyleSheet.create({
},
verticalSurface: {
height: '48%',
+ },
+ verticalSurfaceContent: {
justifyContent: 'center',
},
diff --git a/jest/reanimatedSnapshotSerializer.js b/jest/reanimatedSnapshotSerializer.js
new file mode 100644
index 0000000000..0d20dec218
--- /dev/null
+++ b/jest/reanimatedSnapshotSerializer.js
@@ -0,0 +1,27 @@
+const reanimatedTestProps = new Set([
+ 'jestAnimatedProps',
+ 'jestAnimatedStyle',
+ 'jestInlineStyle',
+]);
+
+module.exports = {
+ test(value) {
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ value.props !== null &&
+ typeof value.props === 'object' &&
+ Object.keys(value.props).some((prop) => reanimatedTestProps.has(prop))
+ );
+ },
+ print(value, serialize) {
+ return serialize({
+ ...value,
+ props: Object.fromEntries(
+ Object.entries(value.props).filter(
+ ([prop]) => !reanimatedTestProps.has(prop)
+ )
+ ),
+ });
+ },
+};
diff --git a/jest/testSetup.js b/jest/testSetup.js
index c00e611084..1033126c45 100644
--- a/jest/testSetup.js
+++ b/jest/testSetup.js
@@ -4,13 +4,10 @@ jest.useFakeTimers();
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
-jest.mock('react-native-worklets', () =>
- require('react-native-worklets/lib/module/mock')
-);
-
-jest.mock('react-native-reanimated', () =>
- require('react-native-reanimated/mock')
-);
+jest.mock('react-native-worklets', () => ({
+ ...require('react-native-worklets/lib/module/mock'),
+ isUIRuntime: () => false,
+}));
jest.mock('@react-native-vector-icons/material-design-icons', () => {
const React = require('react');
diff --git a/package.json b/package.json
index 25ca91fb73..6db11f9fef 100644
--- a/package.json
+++ b/package.json
@@ -111,6 +111,9 @@
"/jest/jestSetupAfterEnv.js",
"@testing-library/react-native"
],
+ "snapshotSerializers": [
+ "/jest/reanimatedSnapshotSerializer.js"
+ ],
"cacheDirectory": "./cache/jest",
"testPathIgnorePatterns": [
"\\.d\\.ts$"
diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx
index 8ef9965819..7e62fd4df9 100644
--- a/src/components/Appbar/Appbar.tsx
+++ b/src/components/Appbar/Appbar.tsx
@@ -1,19 +1,24 @@
import * as React from 'react';
-import { Animated, StyleSheet, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native';
import AppbarContent from './AppbarContent';
import {
getAppbarBackgroundColor,
+ getAppbarBorders,
modeAppbarHeight,
renderAppbarContent,
filterAppbarActions,
} from './utils';
import type { AppbarModes, AppbarChildProps } from './utils';
import { useInternalTheme } from '../../core/theming';
-import type { Elevation, ThemeProp } from '../../types';
+import type { ThemeProp } from '../../types';
import Surface from '../Surface';
+const APPBAR_HORIZONTAL_PADDING = 4;
+
+export type AppbarStyle = Omit;
+
export type Props = Omit, 'style'> & {
/**
* Whether the background color is a dark color. A dark appbar will render light text and vice-versa.
@@ -51,7 +56,7 @@ export type Props = Omit, 'style'> & {
* @optional
*/
theme?: ThemeProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
};
/**
@@ -145,30 +150,26 @@ const Appbar = ({
dark,
style,
mode = 'small',
- elevated,
+ elevated = false,
safeAreaInsets,
theme: themeOverrides,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const flattenedStyle = StyleSheet.flatten(style);
- const {
- backgroundColor: customBackground,
- elevation = elevated ? 2 : 0,
- ...restStyle
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- } = (flattenedStyle || {}) as Exclude & {
- elevation?: Elevation;
+ const { backgroundColor: customBackground, ...restStyle } = (flattenedStyle ||
+ {}) as Exclude & {
backgroundColor?: ColorValue;
};
const backgroundColor = getAppbarBackgroundColor(
theme,
- elevation,
- customBackground,
- elevated
+ elevated,
+ customBackground
);
+ const borderStyles = getAppbarBorders(restStyle);
+
const isMode = (modeToCompare: AppbarModes) => {
return mode === modeToCompare;
};
@@ -210,24 +211,25 @@ const Appbar = ({
const insets = {
paddingBottom: safeAreaInsets?.bottom,
paddingTop: safeAreaInsets?.top,
- paddingLeft: safeAreaInsets?.left,
- paddingRight: safeAreaInsets?.right,
+ paddingLeft: (safeAreaInsets?.left ?? 0) + APPBAR_HORIZONTAL_PADDING,
+ paddingRight: (safeAreaInsets?.right ?? 0) + APPBAR_HORIZONTAL_PADDING,
};
return (
{shouldAddLeftSpacing ? : null}
{(isMode('small') || isMode('center-aligned')) && (
@@ -308,7 +310,6 @@ const styles = StyleSheet.create({
appbar: {
flexDirection: 'row',
alignItems: 'center',
- paddingHorizontal: 4,
},
v3Spacing: {
width: 52,
diff --git a/src/components/Appbar/AppbarAction.tsx b/src/components/Appbar/AppbarAction.tsx
index 400271a988..c6d68a362e 100644
--- a/src/components/Appbar/AppbarAction.tsx
+++ b/src/components/Appbar/AppbarAction.tsx
@@ -1,11 +1,7 @@
import * as React from 'react';
-import type {
- Animated,
- ColorValue,
- StyleProp,
- View,
- ViewStyle,
-} from 'react-native';
+import type { ColorValue, StyleProp, View, ViewStyle } from 'react-native';
+
+import type { AnimatedStyle } from 'react-native-reanimated';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../types';
@@ -43,7 +39,7 @@ export type Props = React.ComponentPropsWithoutRef & {
* Whether it's the leading button. Note: If `Appbar.BackAction` is present, it will be rendered before any `isLeading` icons.
*/
isLeading?: boolean;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp>;
ref?: React.Ref;
/**
* @optional
diff --git a/src/components/Appbar/AppbarBackAction.tsx b/src/components/Appbar/AppbarBackAction.tsx
index 2835c27c91..c4249bd18a 100644
--- a/src/components/Appbar/AppbarBackAction.tsx
+++ b/src/components/Appbar/AppbarBackAction.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import type {
- Animated,
ColorValue,
GestureResponderEvent,
StyleProp,
@@ -8,6 +7,8 @@ import type {
ViewStyle,
} from 'react-native';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
import type { $Omit } from './../../types';
import AppbarAction from './AppbarAction';
import AppbarBackIcon from './AppbarBackIcon';
@@ -36,7 +37,7 @@ export type Props = $Omit<
* Function to execute on press.
*/
onPress?: (e: GestureResponderEvent) => void;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp>;
ref?: React.Ref;
};
diff --git a/src/components/Appbar/AppbarHeader.tsx b/src/components/Appbar/AppbarHeader.tsx
index 0252cc89ba..1c486f9ccd 100644
--- a/src/components/Appbar/AppbarHeader.tsx
+++ b/src/components/Appbar/AppbarHeader.tsx
@@ -1,22 +1,18 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, View } from 'react-native';
-import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
+import { Platform, StyleSheet } from 'react-native';
+import type { ColorValue, StyleProp } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Appbar } from './Appbar';
-import {
- getAppbarBackgroundColor,
- modeAppbarHeight,
- getAppbarBorders,
-} from './utils';
+import type { AppbarStyle } from './Appbar';
+import { getAppbarBackgroundColor, modeAppbarHeight } from './utils';
import { useInternalTheme } from '../../core/theming';
-import { shadow } from '../../theme/tokens/sys/elevation';
import type { ThemeProp } from '../../types';
export type Props = Omit<
React.ComponentProps,
- 'safeAreaInsets'
+ 'safeAreaInsets' | 'style'
> & {
/**
* Whether the background color is a dark color. A dark header will render light text and vice-versa.
@@ -52,7 +48,7 @@ export type Props = Omit<
* @optional
*/
theme?: ThemeProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
};
/**
@@ -100,64 +96,53 @@ const AppbarHeader = ({
const flattenedStyle = StyleSheet.flatten(style);
const {
height = modeAppbarHeight[mode],
- elevation = elevated ? 2 : 0,
backgroundColor: customBackground,
zIndex = elevated ? 1 : 0,
...restStyle
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
} = (flattenedStyle || {}) as Exclude & {
- height?: number;
- elevation?: number;
+ height?: AppbarStyle['height'];
backgroundColor?: ColorValue;
zIndex?: number;
};
- const borderRadius = getAppbarBorders(restStyle);
-
const backgroundColor = getAppbarBackgroundColor(
theme,
- elevation,
- customBackground,
- elevated
+ elevated,
+ customBackground
);
const { top, left, right } = useSafeAreaInsets();
+ const topInset = statusBarHeight ?? top;
+ const horizontalInset = Math.max(left, right);
+ const headerHeight = typeof height === 'number' ? height + topInset : height;
return (
-
-
-
+ safeAreaInsets={{
+ top: topInset,
+ left: horizontalInset,
+ right: horizontalInset,
+ }}
+ dark={dark}
+ elevated={elevated}
+ {...rest}
+ mode={mode}
+ theme={theme}
+ />
);
};
AppbarHeader.displayName = 'Appbar.Header';
-const styles = StyleSheet.create({
- appbar: {
- elevation: 0,
- },
-});
-
export default AppbarHeader;
// @component-docs ignore-next-line
diff --git a/src/components/Appbar/utils.ts b/src/components/Appbar/utils.ts
index a6ea546b34..29978636b9 100644
--- a/src/components/Appbar/utils.ts
+++ b/src/components/Appbar/utils.ts
@@ -1,6 +1,6 @@
import React from 'react';
import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
-import { StyleSheet, Animated } from 'react-native';
+import { StyleSheet } from 'react-native';
import { white } from '../../theme/colors';
import type { InternalTheme, ThemeProp } from '../../types';
@@ -15,17 +15,25 @@ export type AppbarChildProps = {
const borderStyleProperties = [
'borderRadius',
+ 'borderBottomEndRadius',
+ 'borderBottomStartRadius',
+ 'borderEndEndRadius',
+ 'borderEndStartRadius',
+ 'borderStartEndRadius',
+ 'borderStartStartRadius',
+ 'borderTopEndRadius',
+ 'borderTopStartRadius',
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomRightRadius',
'borderBottomLeftRadius',
+ 'borderCurve',
] satisfies readonly (keyof ViewStyle)[];
export const getAppbarBackgroundColor = (
theme: InternalTheme,
- _elevation: number,
- customBackground?: ColorValue,
- elevated?: boolean
+ elevated: boolean,
+ customBackground?: ColorValue
) => {
const { colors } = theme;
if (customBackground) {
@@ -54,19 +62,14 @@ export const getAppbarColor = ({
return undefined;
};
-export const getAppbarBorders = (
- style:
- | Animated.Value
- | Animated.AnimatedInterpolation
- | Animated.WithAnimatedObject
-) => {
- const borders: Record = {};
+export const getAppbarBorders = (style: ViewStyle) => {
+ let borders: ViewStyle = {};
for (const property of borderStyleProperties) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- const value = style[property as keyof typeof style];
- if (value) {
- borders[property] = value;
+ const value = style[property];
+
+ if (typeof value === 'number' || typeof value === 'string') {
+ borders = { ...borders, [property]: value };
}
}
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 869adf6820..4567055f6b 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -1,7 +1,7 @@
import type { StyleProp, TextProps, TextStyle } from 'react-native';
import { StyleSheet } from 'react-native';
-import Animated from 'react-native-reanimated';
+import Animated, { type AnimatedStyle } from 'react-native-reanimated';
import { useInternalTheme } from '../core/theming';
import { cornerFull } from '../theme/tokens/sys/shape';
@@ -21,7 +21,7 @@ export type Props = TextProps & {
* Content of the `Badge`.
*/
children?: string | number;
- style?: StyleProp;
+ style?: StyleProp>;
/**
* @optional
*/
diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx
index f994396931..dddcf330c0 100644
--- a/src/components/Banner.tsx
+++ b/src/components/Banner.tsx
@@ -1,21 +1,37 @@
import * as React from 'react';
-import { Animated, StyleSheet, View } from 'react-native';
-import type { StyleProp, ViewStyle } from 'react-native';
-import type { LayoutChangeEvent } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import type {
+ LayoutChangeEvent,
+ StyleProp,
+ ViewProps,
+ ViewStyle,
+} from 'react-native';
+import Animated, {
+ Easing,
+ interpolate,
+ ReduceMotion,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+import { scheduleOnRN } from 'react-native-worklets';
import useLatestCallback from 'use-latest-callback';
import Button from './Button/Button';
import Icon from './Icon';
import type { IconSource } from './Icon';
import Surface from './Surface';
+import type { SurfaceStyle } from './Surface';
import Text from './Typography/Text';
import { useInternalTheme } from '../core/theming';
-import type { $Omit, $RemoveChildren, ThemeProp } from '../types';
+import type { $RemoveChildren, Elevation, ThemeProp } from '../types';
const DEFAULT_MAX_WIDTH = 960;
-export type Props = $Omit<$RemoveChildren, 'mode'> & {
+type AnimationFinishedCallback = (result: { finished: boolean }) => void;
+
+export type Props = Omit & {
/**
* Whether banner is currently visible.
*/
@@ -51,12 +67,12 @@ export type Props = $Omit<$RemoveChildren, 'mode'> & {
* @supported Available in v5.x with theme version 3
* Changes Banner shadow and background on iOS and Android.
*/
- elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
+ elevation?: Elevation;
/**
* Specifies the largest possible scale a text font can reach.
*/
maxFontSizeMultiplier?: number;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
ref?: React.RefObject;
/**
* @optional
@@ -66,12 +82,12 @@ export type Props = $Omit<$RemoveChildren, 'mode'> & {
* @optional
* Optional callback that will be called after the opening animation finished running normally
*/
- onShowAnimationFinished?: Animated.EndCallback;
+ onShowAnimationFinished?: AnimationFinishedCallback;
/**
* @optional
* Optional callback that will be called after the closing animation finished running normally
*/
- onHideAnimationFinished?: Animated.EndCallback;
+ onHideAnimationFinished?: AnimationFinishedCallback;
};
/**
@@ -134,9 +150,9 @@ const Banner = ({
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { colors } = theme;
- const { current: position } = React.useRef(
- new Animated.Value(visible ? 1 : 0)
- );
+
+ const position = useSharedValue(visible ? 1 : 0);
+
const [layout, setLayout] = React.useState<{
height: number;
measured: boolean;
@@ -149,30 +165,33 @@ const Banner = ({
const hideCallback = useLatestCallback(onHideAnimationFinished);
const { scale } = theme.animation;
-
- const opacity = position.interpolate({
- inputRange: [0, 0.1, 1],
- outputRange: [0, 1, 1],
- });
+ const animationDuration = (visible ? 250 : 200) * scale;
React.useEffect(() => {
- if (visible) {
- // show
- Animated.timing(position, {
- duration: 250 * scale,
- toValue: 1,
- useNativeDriver: false,
- }).start(showCallback);
- } else {
- // hide
- Animated.timing(position, {
- duration: 200 * scale,
- toValue: 0,
- useNativeDriver: false,
- }).start(hideCallback);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [visible, position, scale]);
+ const callback = visible ? showCallback : hideCallback;
+
+ position.value = withTiming(
+ visible ? 1 : 0,
+ {
+ duration: animationDuration,
+ easing: Easing.inOut(Easing.ease),
+ reduceMotion: ReduceMotion.Never,
+ },
+ (finished) => scheduleOnRN(callback, { finished: finished ?? false })
+ );
+ }, [animationDuration, hideCallback, position, showCallback, visible]);
+
+ const surfaceStyle = useAnimatedStyle(() => ({
+ opacity: interpolate(position.value, [0, 0.1, 1], [0, 1, 1]),
+ }));
+
+ const spacerStyle = useAnimatedStyle(() => ({
+ height: position.value * layout.height,
+ }));
+
+ const contentAnimationStyle = useAnimatedStyle(() => ({
+ transform: [{ translateY: (position.value - 1) * layout.height }],
+ }));
const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {
const { height } = nativeEvent.layout;
@@ -186,29 +205,22 @@ const Banner = ({
// Once we have the height, we apply the height to the spacer and switch the banner to position: absolute
// We need this because we need to move the content below as if banner's height was being animated
// However we can't animated banner's height directly as it'll also resize the content inside
- const height = Animated.multiply(position, layout.height);
-
- const translateY = Animated.multiply(
- Animated.add(position, -1),
- layout.height
- );
return (
-
+
({
backgroundColor: customBackground,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
} = (StyleSheet.flatten(style) || {}) as {
- elevation?: number;
backgroundColor?: ColorValue;
};
@@ -462,9 +461,14 @@ const BottomNavigationBar = ({
bottom: safeAreaInsets?.bottom ?? bottom,
};
+ const pointerEvents = layout.measured
+ ? keyboardHidesNavigationBar && keyboardVisible
+ ? 'none'
+ : 'auto'
+ : 'none';
+
return (
- ({
}
: null,
style,
+ { pointerEvents },
]}
- pointerEvents={
- layout.measured
- ? keyboardHidesNavigationBar && keyboardVisible
- ? 'none'
- : 'auto'
- : 'none'
- }
onLayout={onLayout}
- container
>
-
-
+
- {routes.map((route, index) => {
- const focused = navigationState.index === index;
- const active = tabsAnims[index];
-
- // Move down the icon to account for no-label in shifting and smaller label in non-shifting.
- const translateY = labeled
- ? shifting
+
+ {routes.map((route, index) => {
+ const focused = navigationState.index === index;
+ const active = tabsAnims[index];
+
+ // Move down the icon to account for no-label in shifting and smaller label in non-shifting.
+ const translateY = labeled
+ ? shifting
+ ? active.interpolate({
+ inputRange: [0, 1],
+ outputRange: [7, 0],
+ })
+ : 0
+ : 7;
+
+ // We render the active icon and label on top of inactive ones and cross-fade them on change.
+ // This trick gives the illusion that we are animating between active and inactive colors.
+ // This is to ensure that we can use native driver, as colors cannot be animated with native driver.
+ const activeOpacity = active;
+
+ const inactiveOpacity = active.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1, 0],
+ });
+
+ const v3ActiveOpacity = focused ? 1 : 0;
+
+ const v3InactiveOpacity = shifting
+ ? inactiveOpacity
+ : focused
+ ? 0
+ : 1;
+
+ // Scale horizontally the outline pill
+ const outlineScale = focused
? active.interpolate({
inputRange: [0, 1],
- outputRange: [7, 0],
+ outputRange: [0.5, 1],
})
- : 0
- : 7;
-
- // We render the active icon and label on top of inactive ones and cross-fade them on change.
- // This trick gives the illusion that we are animating between active and inactive colors.
- // This is to ensure that we can use native driver, as colors cannot be animated with native driver.
- const activeOpacity = active;
- const inactiveOpacity = active.interpolate({
- inputRange: [0, 1],
- outputRange: [1, 0],
- });
-
- const v3ActiveOpacity = focused ? 1 : 0;
- const v3InactiveOpacity = shifting
- ? inactiveOpacity
- : focused
- ? 0
- : 1;
-
- // Scale horizontally the outline pill
- const outlineScale = focused
- ? active.interpolate({
- inputRange: [0, 1],
- outputRange: [0.5, 1],
- })
- : 0;
-
- const badge = getBadge({ route });
-
- const activeLabelColor = getLabelColor({
- tintColor: activeTintColor,
- hasColor: Boolean(activeColor),
- focused,
- theme,
- });
-
- const inactiveLabelColor = getLabelColor({
- tintColor: inactiveTintColor,
- hasColor: Boolean(inactiveColor),
- focused,
- theme,
- });
-
- const badgeStyle = {
- top: typeof badge === 'boolean' ? 4 : 2,
- right:
- badge != null && typeof badge !== 'boolean'
- ? String(badge).length * -2
- : 0,
- };
-
- const isLegacyOrV3Shifting = shifting && labeled;
-
- const font = theme.fonts.labelMedium;
-
- return renderTouchable({
- key: route.key,
- route,
- borderless: true,
- centered: true,
- rippleColor: 'transparent',
- onPress: () => onTabPress(eventForIndex(index)),
- onLongPress: () => onTabLongPress?.(eventForIndex(index)),
- testID: getTestID({ route }),
- 'aria-label': getAccessibilityLabel({ route }),
- role: Platform.OS === 'ios' ? 'button' : 'tab',
- 'aria-selected': focused,
- style: [styles.item, styles.v3Item],
- children: (
-
- onTabPress(eventForIndex(index)),
+ onLongPress: () => onTabLongPress?.(eventForIndex(index)),
+ testID: getTestID({ route }),
+ 'aria-label': getAccessibilityLabel({ route }),
+ role: Platform.OS === 'ios' ? 'button' : 'tab',
+ 'aria-selected': focused,
+ style: [styles.item, styles.v3Item],
+ children: (
+
- {focused && (
-
- )}
-
- {renderIcon ? (
- renderIcon({
- route,
- focused: true,
- color: activeTintColor,
- })
- ) : (
-
- )}
-
- {renderIcon ? (
- renderIcon({
- route,
- focused: false,
- color: inactiveTintColor,
- })
- ) : (
-
)}
-
-
- {typeof badge === 'boolean' ? (
-
- ) : (
- {badge}
- )}
-
-
- {labeled ? (
-
({
},
]}
>
- {renderLabel ? (
- renderLabel({
+ {renderIcon ? (
+ renderIcon({
route,
focused: true,
- color: activeLabelColor,
+ color: activeTintColor,
})
) : (
-
- {getLabelText({ route })}
-
+
)}
- {shifting ? null : (
+
+ {renderIcon ? (
+ renderIcon({
+ route,
+ focused: false,
+ color: inactiveTintColor,
+ })
+ ) : (
+
+ )}
+
+
+ {typeof badge === 'boolean' ? (
+
+ ) : (
+ {badge}
+ )}
+
+
+ {labeled ? (
+
{renderLabel ? (
renderLabel({
route,
- focused: false,
- color: inactiveLabelColor,
+ focused: true,
+ color: activeLabelColor,
})
) : (
({
)}
- )}
-
- ) : null}
-
- ),
- });
- })}
-
-
-
+ {shifting ? null : (
+
+ {renderLabel ? (
+ renderLabel({
+ route,
+ focused: false,
+ color: inactiveLabelColor,
+ })
+ ) : (
+
+ {getLabelText({ route })}
+
+ )}
+
+ )}
+
+ ) : null}
+
+ ),
+ });
+ })}
+
+
+
+
);
};
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index bfc7782d66..00f0242f0f 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
@@ -7,24 +7,25 @@ import type {
Role,
StyleProp,
TextStyle,
+ ViewProps,
ViewStyle,
} from 'react-native';
import { getButtonColors, getButtonTouchableRippleStyle } from './utils';
import type { ButtonMode } from './utils';
import { useInternalTheme } from '../../core/theming';
-import type { $Omit, ThemeProp } from '../../types';
+import type { ThemeProp } from '../../types';
import hasTouchHandler from '../../utils/hasTouchHandler';
-import { splitStyles } from '../../utils/splitStyles';
import ActivityIndicator from '../ActivityIndicator';
import Icon from '../Icon';
import type { IconSource } from '../Icon';
import Surface from '../Surface';
+import type { SurfaceStyle } from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
-export type Props = $Omit, 'mode'> & {
+export type Props = Omit & {
/**
* Mode of the button. You can change the mode to adjust the styling to give it desired emphasis.
* - `text` - flat button without background or outline, used for the lowest priority actions, especially when presenting multiple options.
@@ -122,7 +123,7 @@ export type Props = $Omit, 'mode'> & {
* Sets additional distance outside of element in which a press can be detected.
*/
hitSlop?: TouchableRippleProps['hitSlop'];
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
/**
* Style for the button text.
*/
@@ -192,15 +193,15 @@ const Button = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
+
const isMode = React.useCallback(
(modeToCompare: ButtonMode) => {
return mode === modeToCompare;
},
[mode]
);
- const { animation } = theme;
+
const uppercase = uppercaseProp ?? false;
- const isWeb = Platform.OS === 'web';
const hasPassedTouchHandler = hasTouchHandler({
onPress,
@@ -213,52 +214,33 @@ const Button = ({
const initialElevation = 1;
const activeElevation = 2;
- const { current: elevation } = React.useRef(
- new Animated.Value(isElevationEntitled ? initialElevation : 0)
- );
+ const [pressed, setPressed] = React.useState(false);
- React.useEffect(() => {
- // Workaround not to call setValue on Animated.Value, because it breaks styles.
- // https://github.com/callstack/react-native-paper/issues/4559
- Animated.timing(elevation, {
- toValue: isElevationEntitled ? initialElevation : 0,
- duration: 0,
- useNativeDriver: true,
- });
- }, [isElevationEntitled, elevation, initialElevation]);
+ const elevation = isElevationEntitled
+ ? pressed
+ ? activeElevation
+ : initialElevation
+ : 0;
const handlePressIn = (e: GestureResponderEvent) => {
onPressIn?.(e);
- if (isMode('elevated')) {
- const { scale } = animation;
- Animated.timing(elevation, {
- toValue: activeElevation,
- duration: 200 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
+
+ if (isElevationEntitled) {
+ setPressed(true);
}
};
const handlePressOut = (e: GestureResponderEvent) => {
onPressOut?.(e);
- if (isMode('elevated')) {
- const { scale } = animation;
- Animated.timing(elevation, {
- toValue: initialElevation,
- duration: 150 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
+
+ if (isElevationEntitled) {
+ setPressed(false);
}
};
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- const flattenedStyles = (StyleSheet.flatten(style) || {}) as ViewStyle;
- const [, borderRadiusStyles] = splitStyles(
- flattenedStyles,
- (style) => style.startsWith('border') && style.endsWith('Radius')
- );
+ const elevationTransitionDuration =
+ theme.motion.duration[pressed ? 'short4' : 'short3'] *
+ theme.animation.scale;
const borderRadius = theme.shapes.corner.largeIncreased;
const iconSize = 18;
@@ -279,17 +261,7 @@ const Button = ({
dark,
});
- const touchableStyle = {
- ...borderRadiusStyles,
- borderRadius: borderRadiusStyles.borderRadius ?? borderRadius,
- };
-
- const buttonStyle = {
- backgroundColor: backgroundOpacity < 1 ? 'transparent' : backgroundColor,
- borderColor,
- borderWidth,
- ...touchableStyle,
- };
+ const touchableStyle = { borderRadius };
const { color: customLabelColor, fontSize: customLabelSize } =
StyleSheet.flatten(labelStyle) || {};
@@ -321,9 +293,19 @@ const Button = ({
{...rest}
ref={ref}
testID={`${testID}-container`}
- style={[styles.button, compact && styles.compact, buttonStyle, style]}
+ backgroundColor={backgroundOpacity < 1 ? 'transparent' : backgroundColor}
+ {...touchableStyle}
+ style={[
+ styles.button,
+ compact && styles.compact,
+ {
+ borderColor,
+ borderWidth,
+ },
+ style,
+ ]}
elevation={elevation}
- container
+ transitionDuration={elevationTransitionDuration}
>
{backgroundOpacity < 1 && (
, 'mode'> & {
+export type Props = Omit & {
/**
* Mode of the Card.
* - `elevated` - Card with elevation.
@@ -73,12 +76,12 @@ export type Props = $Omit, 'mode'> & {
/**
* Changes Card shadow and background on iOS and Android.
*/
- elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
+ elevation?: Elevation;
/**
* Style of card's inner content.
*/
contentStyle?: StyleProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
/**
* @optional
*/
@@ -91,6 +94,10 @@ export type Props = $Omit, 'mode'> & {
* Pass down accessible from card props to touchable
*/
accessible?: boolean;
+ /**
+ * Reference to the card container.
+ */
+ ref?: React.Ref;
};
/**
@@ -141,6 +148,7 @@ const Card = ({
...rest
}: (OutlinedCardProps | ElevatedCardProps | ContainedCardProps) & Props) => {
const theme = useInternalTheme(themeOverrides);
+
const isMode = React.useCallback(
(modeToCompare: Mode) => {
return cardMode === modeToCompare;
@@ -155,34 +163,23 @@ const Card = ({
onPressOut,
});
- const { current: elevation } = React.useRef(
- new Animated.Value(cardElevation)
- );
- const { animation } = theme;
-
- const animationDuration = 150 * animation.scale;
-
- const runElevationAnimation = (pressType: HandlePressType) => {
- if (isMode('contained')) {
- return;
- }
-
- const isPressTypeIn = pressType === 'in';
- Animated.timing(elevation, {
- toValue: isPressTypeIn ? 2 : cardElevation,
- duration: animationDuration,
- useNativeDriver: false,
- }).start();
- };
+ const [pressed, setPressed] = React.useState(false);
+ const elevation = isMode('elevated') ? (pressed ? 2 : cardElevation) : 0;
const handlePressIn = useLatestCallback((e: GestureResponderEvent) => {
onPressIn?.(e);
- runElevationAnimation('in');
+
+ if (isMode('elevated')) {
+ setPressed(true);
+ }
});
const handlePressOut = useLatestCallback((e: GestureResponderEvent) => {
onPressOut?.(e);
- runElevationAnimation('out');
+
+ if (isMode('elevated')) {
+ setPressed(false);
+ }
});
const total = React.Children.count(children);
@@ -204,15 +201,7 @@ const Card = ({
const { borderColor = themedBorderColor } = flattenedStyles;
- const [, borderRadiusStyles] = splitStyles(
- flattenedStyles,
- (style) => style.startsWith('border') && style.endsWith('Radius')
- );
-
- const borderRadiusCombinedStyles = {
- borderRadius: theme.shapes.corner.medium,
- ...borderRadiusStyles,
- };
+ const borderRadius = theme.shapes.corner.medium;
const content = (
@@ -222,7 +211,6 @@ const Card = ({
index,
total,
siblings,
- borderRadiusStyles,
})
: child
)}
@@ -232,15 +220,12 @@ const Card = ({
return (
{isMode('outlined') && (
@@ -252,7 +237,7 @@ const Card = ({
borderColor,
},
styles.outline,
- borderRadiusCombinedStyles,
+ { borderRadius },
]}
/>
)}
diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx
index b6482e9209..3bbbe32cf5 100644
--- a/src/components/Chip/Chip.tsx
+++ b/src/components/Chip/Chip.tsx
@@ -1,12 +1,12 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native';
+import { Platform, StyleSheet, Pressable, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
PressableAndroidRippleConfig,
StyleProp,
TextStyle,
- ViewStyle,
+ ViewProps,
} from 'react-native';
import useLatestCallback from 'use-latest-callback';
@@ -15,17 +15,18 @@ import { getChipColors } from './helpers';
import type { ChipAvatarProps } from './helpers';
import { useInternalTheme } from '../../core/theming';
import { white } from '../../theme/colors';
-import type { $Omit, EllipsizeProp, ThemeProp } from '../../types';
+import type { EllipsizeProp, ThemeProp } from '../../types';
import hasTouchHandler from '../../utils/hasTouchHandler';
import type { IconSource } from '../Icon';
import Icon from '../Icon';
import MaterialCommunityIcon from '../MaterialCommunityIcon';
import Surface from '../Surface';
+import type { SurfaceStyle } from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
-export type Props = $Omit, 'mode'> & {
+export type Props = Omit & {
/**
* Mode of the chip.
* - `flat` - flat chip without outline.
@@ -123,7 +124,7 @@ export type Props = $Omit, 'mode'> & {
* Style of chip's text
*/
textStyle?: StyleProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
/**
* Sets additional distance outside of element in which a press can be detected.
*/
@@ -144,6 +145,10 @@ export type Props = $Omit, 'mode'> & {
* Specifies the largest possible scale a text font can reach.
*/
maxFontSizeMultiplier?: number;
+ /**
+ * Reference to the chip container.
+ */
+ ref?: React.Ref;
};
/**
@@ -201,11 +206,9 @@ const Chip = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
- const isWeb = Platform.OS === 'web';
- const { current: elevation } = React.useRef(
- new Animated.Value(elevated ? 1 : 0)
- );
+ const [pressed, setPressed] = React.useState(false);
+ const elevation = elevated ? (pressed ? 2 : 1) : 0;
const hasPassedTouchHandler = hasTouchHandler({
onPress,
@@ -217,36 +220,24 @@ const Chip = ({
const isOutlined = mode === 'outlined';
const handlePressIn = useLatestCallback((e: GestureResponderEvent) => {
- const { scale } = theme.animation;
onPressIn?.(e);
- Animated.timing(elevation, {
- toValue: elevated ? 2 : 0,
- duration: 200 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
+ setPressed(true);
});
const handlePressOut = useLatestCallback((e: GestureResponderEvent) => {
- const { scale } = theme.animation;
onPressOut?.(e);
- Animated.timing(elevation, {
- toValue: elevated ? 1 : 0,
- duration: 150 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
+ setPressed(false);
});
+ const elevationTransitionDuration =
+ theme.motion.duration[pressed ? 'short4' : 'short3'] *
+ theme.animation.scale;
+
const opacity = 0.38;
const defaultBorderRadius = theme.shapes.corner.small;
const iconSize = 18;
- const {
- backgroundColor: customBackgroundColor,
- borderRadius = defaultBorderRadius,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- } = (StyleSheet.flatten(style) || {}) as ViewStyle;
+ const borderRadius = defaultBorderRadius;
const {
borderColor,
@@ -259,12 +250,11 @@ const Chip = ({
isOutlined,
theme,
selectedColor,
- customBackgroundColor,
disabled,
});
- const elevationStyle = elevation;
const multiplier = compact ? 1.5 : 2;
+
const labelSpacings = {
marginRight: onClose ? 0 : 8 * multiplier,
marginLeft:
@@ -272,30 +262,26 @@ const Chip = ({
? 4 * multiplier
: 8 * multiplier,
};
+
const contentSpacings = {
paddingRight: onClose ? 34 : 0,
};
+
const labelTextStyle = {
color: textColor,
...theme.fonts.labelLarge,
};
+
return (
{
+}: BaseProps & { selectedColor?: ColorValue }) => {
const isSelectedColor = selectedColor !== undefined;
const { colors } = md3(theme);
@@ -85,19 +85,8 @@ const getDefaultBackgroundColor = ({
return colors.secondaryContainer;
};
-const getBackgroundColor = ({
- theme,
- isOutlined,
- disabled,
- customBackgroundColor,
-}: BaseProps & {
- customBackgroundColor?: ColorValue;
-}) => {
+const getBackgroundColor = ({ theme, isOutlined, disabled }: BaseProps) => {
const { colors } = md3(theme);
- if (typeof customBackgroundColor === 'string') {
- return customBackgroundColor;
- }
-
if (disabled) {
if (isOutlined) {
return 'transparent';
@@ -108,22 +97,6 @@ const getBackgroundColor = ({
return getDefaultBackgroundColor({ theme, isOutlined });
};
-const getSelectedBackgroundColor = ({
- theme,
- isOutlined,
- disabled,
- customBackgroundColor,
-}: BaseProps & {
- customBackgroundColor?: ColorValue;
-}) => {
- return getBackgroundColor({
- theme,
- disabled,
- isOutlined,
- customBackgroundColor,
- });
-};
-
const getIconColor = ({
theme,
isOutlined,
@@ -153,10 +126,8 @@ export const getChipColors = ({
isOutlined,
theme,
selectedColor,
- customBackgroundColor,
disabled,
}: BaseProps & {
- customBackgroundColor?: ColorValue;
disabled?: boolean;
selectedColor?: ColorValue;
}) => {
@@ -164,12 +135,6 @@ export const getChipColors = ({
const backgroundColor = getBackgroundColor({
...baseChipColorProps,
- customBackgroundColor,
- });
-
- const selectedBackgroundColor = getSelectedBackgroundColor({
- ...baseChipColorProps,
- customBackgroundColor,
});
const contentOpacity = disabled
@@ -180,7 +145,6 @@ export const getChipColors = ({
borderColor: getBorderColor({
...baseChipColorProps,
selectedColor,
- backgroundColor,
}),
textColor: getTextColor({
...baseChipColorProps,
@@ -192,6 +156,6 @@ export const getChipColors = ({
}),
contentOpacity,
backgroundColor,
- selectedBackgroundColor,
+ selectedBackgroundColor: backgroundColor,
};
};
diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx
index a7b082fe89..89832fe658 100644
--- a/src/components/Dialog/Dialog.tsx
+++ b/src/components/Dialog/Dialog.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet } from 'react-native';
-import type { StyleProp, ViewStyle } from 'react-native';
+import { Platform, StyleSheet } from 'react-native';
+import type { StyleProp } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -10,8 +10,9 @@ import DialogIcon from './DialogIcon';
import DialogScrollArea from './DialogScrollArea';
import DialogTitle from './DialogTitle';
import { useInternalTheme } from '../../core/theming';
-import type { ThemeProp } from '../../types';
+import type { Elevation, ThemeProp } from '../../types';
import Modal from '../Modal';
+import type { SurfaceStyle } from '../Surface';
import type { DialogChildProps } from './utils';
export type Props = {
@@ -35,7 +36,7 @@ export type Props = {
* Content of the `Dialog`.
*/
children: React.ReactNode;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
/**
* @optional
*/
@@ -46,7 +47,7 @@ export type Props = {
testID?: string;
};
-const DIALOG_ELEVATION: number = 24;
+const DIALOG_ELEVATION: Elevation = 3;
/**
* Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks.
@@ -99,6 +100,7 @@ const Dialog = ({
testID,
}: Props) => {
const { right, left } = useSafeAreaInsets();
+
const theme = useInternalTheme(themeOverrides);
const borderRadius = theme.shapes.corner.extraLarge;
@@ -110,10 +112,11 @@ const Dialog = ({
dismissableBackButton={dismissableBackButton}
onDismiss={onDismiss}
visible={visible}
+ contentBackgroundColor={backgroundColor}
+ contentBorderRadius={borderRadius}
+ contentElevation={DIALOG_ELEVATION}
contentContainerStyle={[
{
- borderRadius,
- backgroundColor,
marginHorizontal: Math.max(left, right, 26),
},
styles.container,
@@ -158,7 +161,6 @@ const styles = StyleSheet.create({
* dialog (44 pixel from the top and bottom) it won't be dismissed.
*/
marginVertical: Platform.OS === 'android' ? 44 : 0,
- elevation: DIALOG_ELEVATION,
justifyContent: 'flex-start',
},
});
diff --git a/src/components/FAB/Extended.tsx b/src/components/FAB/Extended.tsx
index 06a67cab1f..ff3c500545 100644
--- a/src/components/FAB/Extended.tsx
+++ b/src/components/FAB/Extended.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { StyleSheet, View } from 'react-native';
+import { Platform, StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
@@ -8,14 +8,15 @@ import type {
ViewStyle,
} from 'react-native';
-import Reanimated, {
+import Animated, {
+ type AnimatedStyle,
measure,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
-import { scheduleOnUI } from 'react-native-worklets';
+import { isUIRuntime, scheduleOnUI } from 'react-native-worklets';
import Shell from './Shell';
import type { Size, Variant } from './tokens';
@@ -101,7 +102,7 @@ export type Props = {
* Style for positioning the FAB. The visual treatment (size, shape, color)
* is driven by `variant` and `size`.
*/
- style?: StyleProp;
+ style?: StyleProp>;
/**
* TestID used for testing purposes.
*/
@@ -177,7 +178,7 @@ const Extended = ({
const dimensions = getDimensions({ theme, size });
- const offscreenLabelRef = useAnimatedRef();
+ const offscreenLabelRef = useAnimatedRef();
const widthValue = useSharedValue(dimensions.width);
const labelOpacity = useSharedValue(expanded ? 1 : 0);
@@ -190,15 +191,20 @@ const Extended = ({
iconLabelGap,
trailing,
} = dimensions;
+
const targetOpacity = expanded ? 1 : 0;
if (reduceMotion) {
scheduleOnUI(() => {
'worklet';
- const m = measure(offscreenLabelRef);
- const lw = m?.width ?? 0;
+
+ const labelWidth =
+ Platform.OS === 'web' || isUIRuntime()
+ ? (measure(offscreenLabelRef)?.width ?? 0)
+ : 0;
+
widthValue.value = expanded
- ? leading + iconSize + iconLabelGap + lw + trailing
+ ? leading + iconSize + iconLabelGap + labelWidth + trailing
: collapsedWidth;
labelOpacity.value = targetOpacity;
});
@@ -218,9 +224,15 @@ const Extended = ({
scheduleOnUI(() => {
'worklet';
- const m = measure(offscreenLabelRef);
- const lw = m?.width ?? 0;
- const expandedWidth = leading + iconSize + iconLabelGap + lw + trailing;
+
+ const labelWidth =
+ Platform.OS === 'web' || isUIRuntime()
+ ? (measure(offscreenLabelRef)?.width ?? 0)
+ : 0;
+
+ const expandedWidth =
+ leading + iconSize + iconLabelGap + labelWidth + trailing;
+
widthValue.value = withSpring(
expanded ? expandedWidth : collapsedWidth,
widthSpring
@@ -267,7 +279,7 @@ const Extended = ({
testID={testID}
theme={themeOverrides}
/>
-
{label}
-
+
>
);
};
diff --git a/src/components/FAB/FAB.tsx b/src/components/FAB/FAB.tsx
index fa4de1288c..fac6460c6a 100644
--- a/src/components/FAB/FAB.tsx
+++ b/src/components/FAB/FAB.tsx
@@ -8,6 +8,8 @@ import type {
ViewStyle,
} from 'react-native';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
import Shell from './Shell';
import type { Size, Variant } from './tokens';
import type { ThemeProp } from '../../types';
@@ -73,7 +75,7 @@ export type Props = {
* Style for positioning the FAB. The visual treatment (size, shape, color)
* is driven by `variant` and `size`.
*/
- style?: StyleProp;
+ style?: StyleProp>;
/**
* TestID used for testing purposes.
*/
diff --git a/src/components/FAB/Shell.tsx b/src/components/FAB/Shell.tsx
index 2d2d60e4cd..b9f6279194 100644
--- a/src/components/FAB/Shell.tsx
+++ b/src/components/FAB/Shell.tsx
@@ -8,9 +8,10 @@ import type {
ViewStyle,
} from 'react-native';
-import Reanimated, {
+import Animated, {
useAnimatedStyle,
useSharedValue,
+ withSpring,
} from 'react-native-reanimated';
import type { SharedValue } from 'react-native-reanimated';
import type { AnimatedStyle } from 'react-native-reanimated';
@@ -24,12 +25,14 @@ import {
} from './tokens';
import type { Size, Variant } from './tokens';
import { useFocusRing } from './useFocusRing';
-import { useVisibility } from './useVisibility';
import { getDimensions, resolveColors } from './utils';
import { useInternalTheme } from '../../core/theming';
+import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
+import { toRawSpring } from '../../theme/tokens/sys/motion';
import type { ShapeToken } from '../../theme/utils/shape';
import type { Elevation, ThemeProp } from '../../types';
import type { IconSource } from '../Icon';
+import Surface from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
export type ShellProps = {
@@ -159,7 +162,7 @@ export type ShellProps = {
* Outer-positioning style. Visual treatment (size, shape, color) comes from
* `variant` and `size`.
*/
- style?: StyleProp;
+ style?: StyleProp>;
/**
* TestID used for testing purposes.
*/
@@ -174,7 +177,7 @@ export type ShellProps = {
/**
* Internal shell used by every FAB-flavored component (regular, Extended,
* morphing menu trigger). Owns the outer container, ripple, clip, and the
- * visibility animation (scale + alpha + shadow). Consumers that need to
+ * visibility animation (scale + alpha). Consumers that need to
* animate the outer's width/height/borderRadius pass shared values; the
* static size-driven defaults are used otherwise.
*
@@ -225,11 +228,30 @@ const Shell = ({
[theme, variant, containerColor, contentColor]
);
- const { scale, alpha, shadowStyle } = useVisibility({
- visible,
- theme,
- elevation,
- });
+ const reduceMotion = useReduceMotion();
+
+ const scale = useSharedValue(visible ? 1 : 0);
+ const alpha = useSharedValue(visible ? 1 : 0);
+
+ React.useEffect(() => {
+ const target = visible ? 1 : 0;
+
+ if (reduceMotion) {
+ scale.value = target;
+ alpha.value = target;
+ return;
+ }
+
+ scale.value = withSpring(
+ target,
+ toRawSpring(theme.motion.spring.fast.spatial)
+ );
+
+ alpha.value = withSpring(
+ target,
+ toRawSpring(theme.motion.spring.fast.effects)
+ );
+ }, [visible, theme, reduceMotion, scale, alpha]);
// Fallback shared values track the static size-driven dimensions. Consumers
// that don't supply their own animated shared values get these. Keeping
@@ -238,6 +260,7 @@ const Shell = ({
const fallbackWidth = useSharedValue(dimensions.width);
const fallbackHeight = useSharedValue(dimensions.height);
const fallbackBorderRadius = useSharedValue(dimensions.borderRadius);
+
React.useEffect(() => {
fallbackWidth.value = dimensions.width;
fallbackHeight.value = dimensions.height;
@@ -262,10 +285,8 @@ const Shell = ({
opacity: alpha.value,
width: width.value,
height: height.value,
- borderRadius: borderRadius.value,
- backgroundColor: containerBg,
}),
- [width, height, borderRadius, containerBg]
+ [width, height]
);
const clipStyle = useAnimatedStyle(
@@ -277,6 +298,7 @@ const Shell = ({
);
const { focusedSV, onFocus, onBlur } = useFocusRing();
+
const focusRingStyle = useAnimatedStyle(
() => ({
opacity: focusedSV.value ? 1 : 0,
@@ -286,18 +308,21 @@ const Shell = ({
);
return (
-
-
+
{overlay}
)}
-
-
+
-
+
);
};
diff --git a/src/components/FAB/useVisibility.ts b/src/components/FAB/useVisibility.ts
deleted file mode 100644
index f6c4b2e645..0000000000
--- a/src/components/FAB/useVisibility.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import * as React from 'react';
-import { Platform, type ViewStyle } from 'react-native';
-
-import {
- useAnimatedStyle,
- useSharedValue,
- withSpring,
- type AnimatedStyle,
- type SharedValue,
-} from 'react-native-reanimated';
-
-import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
-import {
- androidElevationLevels,
- shadow,
- shadowLayers,
-} from '../../theme/tokens/sys/elevation';
-import { toRawSpring } from '../../theme/tokens/sys/motion';
-import type { Elevation, InternalTheme } from '../../types';
-
-type UseVisibilityArgs = {
- visible: boolean;
- theme: InternalTheme;
- initialScale?: number;
- transformOrigin?: ViewStyle['transformOrigin'];
- /**
- * Elevation level when shown. Shadow fades in/out with the FAB.
- */
- elevation?: Elevation;
-};
-
-type UseVisibilityResult = {
- scale: SharedValue;
- alpha: SharedValue;
- transformOrigin: ViewStyle['transformOrigin'];
- shadowStyle: AnimatedStyle;
-};
-
-/**
- * Animates a FAB in and out: scale + alpha together.
- * Reduce-motion: snap to the final value, no animation.
- *
- * Returns `shadowStyle` too. Put it on the same view as the transform so the
- * shadow stays in sync (Android uses `elevation`, iOS uses `shadow*`, Web uses
- * `boxShadow` -- the outer container's `opacity: alpha.value` handles the
- * visibility fade on Web so the shadow string can be static).
- */
-export function useVisibility({
- visible,
- theme,
- initialScale = 0,
- transformOrigin = 'center',
- elevation = 0,
-}: UseVisibilityArgs): UseVisibilityResult {
- const reduceMotion = useReduceMotion();
- const scale = useSharedValue(visible ? 1 : initialScale);
- const alpha = useSharedValue(visible ? 1 : 0);
-
- React.useEffect(() => {
- const targetScale = visible ? 1 : initialScale;
- const targetAlpha = visible ? 1 : 0;
- if (reduceMotion) {
- scale.value = targetScale;
- alpha.value = targetAlpha;
- return;
- }
- scale.value = withSpring(
- targetScale,
- toRawSpring(theme.motion.spring.fast.spatial)
- );
- alpha.value = withSpring(
- targetAlpha,
- toRawSpring(theme.motion.spring.fast.effects)
- );
- }, [visible, theme, reduceMotion, scale, alpha, initialScale]);
-
- const restingElevationDp = androidElevationLevels[elevation];
- const shadowOffsetHeight = shadowLayers[0].height[elevation];
- const shadowRadius = shadowLayers[0].shadowRadius[elevation];
- const shadowOpacity = elevation ? shadowLayers[0].shadowOpacity : 0;
- const shadowColor = theme.colors.shadow;
-
- const webShadow =
- Platform.OS === 'web' ? shadow(elevation, shadowColor) : null;
-
- const shadowStyle = useAnimatedStyle(() => {
- if (Platform.OS === 'android') {
- return { elevation: alpha.value * restingElevationDp };
- }
- if (Platform.OS === 'web') {
- return webShadow ?? {};
- }
- return {
- shadowColor,
- shadowOpacity: alpha.value * shadowOpacity,
- shadowOffset: { width: 0, height: shadowOffsetHeight },
- shadowRadius,
- };
- });
-
- return { scale, alpha, transformOrigin, shadowStyle };
-}
diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx
index 270c9289ac..52cb5fa3b2 100644
--- a/src/components/IconButton/IconButton.tsx
+++ b/src/components/IconButton/IconButton.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Animated, StyleSheet, View } from 'react-native';
+import { StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
@@ -7,6 +7,8 @@ import type {
ViewStyle,
} from 'react-native';
+import Animated, { type AnimatedStyle } from 'react-native-reanimated';
+
import { getIconButtonColor } from './utils';
import { useInternalTheme } from '../../core/theming';
import type { $RemoveChildren, ThemeProp } from '../../types';
@@ -14,7 +16,6 @@ import ActivityIndicator from '../ActivityIndicator';
import CrossFadeIcon from '../CrossFadeIcon';
import Icon from '../Icon';
import type { IconSource } from '../Icon';
-import Surface from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
const PADDING = 8;
@@ -69,7 +70,7 @@ export type Props = Omit<$RemoveChildren, 'style'> & {
* Function to execute on press.
*/
onPress?: (e: GestureResponderEvent) => void;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp>;
ref?: React.Ref;
/**
* TestID used for testing purposes
@@ -147,34 +148,26 @@ const IconButton = ({
const buttonSize = size + 2 * PADDING;
- const {
- borderWidth = mode === 'outlined' && !selected ? 1 : 0,
- borderRadius = buttonSize / 2,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- } = (StyleSheet.flatten(style) || {}) as ViewStyle;
-
const borderStyles = {
- borderWidth,
- borderRadius,
+ borderWidth: mode === 'outlined' && !selected ? 1 : 0,
+ borderRadius: buttonSize / 2,
borderColor,
};
return (
-
{backgroundOpacity < 1 && (
-
+
);
};
const styles = StyleSheet.create({
container: {
- overflow: 'hidden',
margin: 6,
- elevation: 0,
+ overflow: 'hidden',
},
touchable: {
flexGrow: 1,
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
index ec4c256dad..63fb0faf1f 100644
--- a/src/components/Menu/Menu.tsx
+++ b/src/components/Menu/Menu.tsx
@@ -1,14 +1,12 @@
import * as React from 'react';
import {
- Animated,
Dimensions,
- Easing,
Keyboard,
Platform,
+ Pressable,
ScrollView,
StyleSheet,
View,
- Pressable,
} from 'react-native';
import type { KeyboardEvent as RNKeyboardEvent } from 'react-native';
import type {
@@ -20,7 +18,16 @@ import type {
ViewStyle,
} from 'react-native';
+import Animated, {
+ Easing,
+ ReduceMotion,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { scheduleOnRN } from 'react-native-worklets';
+import useLatestCallback from 'use-latest-callback';
import MenuItem from './MenuItem';
import { useLocale } from '../../core/locale';
@@ -30,6 +37,7 @@ import { addEventListener } from '../../utils/addEventListener';
import { BackHandler } from '../../utils/BackHandler/BackHandler';
import Portal from '../Portal/Portal';
import Surface from '../Surface';
+import type { SurfaceStyle } from '../Surface';
export type Props = {
/**
@@ -67,7 +75,7 @@ export type Props = {
/**
* Style of menu's inner content.
*/
- contentStyle?: Animated.WithAnimatedValue>;
+ contentStyle?: StyleProp;
style?: StyleProp;
/**
* Elevation level of the menu's content. Shadow styles are calculated based on this value. Default `backgroundColor` is taken from the corresponding `theme.colors.elevation` property. By default equals `2`.
@@ -127,8 +135,6 @@ const isCoordinate = (anchor: any): anchor is { x: number; y: number } =>
typeof anchor?.x === 'number' &&
typeof anchor?.y === 'number';
-const isBrowser = () => Platform.OS === 'web' && 'document' in global;
-
/**
* Menus display a list of choices on temporary elevated surfaces. Their placement varies based on the element that opens them.
*
@@ -193,9 +199,10 @@ const Menu = ({
keyboardShouldPersistTaps,
}: Props) => {
const theme = useInternalTheme(themeOverrides);
+
const { direction } = useLocale();
- const { colors: md3Colors } = theme;
const insets = useSafeAreaInsets();
+
const [rendered, setRendered] = React.useState(visible);
const [left, setLeft] = React.useState(0);
const [top, setTop] = React.useState(0);
@@ -209,13 +216,15 @@ const Menu = ({
height: WINDOW_LAYOUT.height,
});
- const opacityAnimationRef = React.useRef(new Animated.Value(0));
- const scaleAnimationRef = React.useRef(new Animated.ValueXY({ x: 0, y: 0 }));
+ const opacity = useSharedValue(0);
+ const scaleX = useSharedValue(0);
+ const scaleY = useSharedValue(0);
+
const keyboardHeightRef = React.useRef(0);
const prevVisible = React.useRef(null);
const anchorRef = React.useRef(null);
const menuRef = React.useRef(null);
- const prevRendered = React.useRef(false);
+ const isShownRef = React.useRef(false);
const keyboardDidShow = React.useCallback((e: RNKeyboardEvent) => {
const keyboardHeight = e.endCoordinates.height;
@@ -258,7 +267,10 @@ const Menu = ({
const removeListeners = React.useCallback(() => {
backHandlerSubscriptionRef.current?.remove();
dimensionsSubscriptionRef.current?.remove();
- isBrowser() && document.removeEventListener('keyup', handleKeypress);
+
+ if (Platform.OS === 'web' && 'document' in global) {
+ document.removeEventListener('keyup', handleKeypress);
+ }
}, [handleKeypress]);
const attachListeners = React.useCallback(() => {
@@ -301,6 +313,26 @@ const Menu = ({
[anchor]
);
+ const handleShowAnimationFinished = useLatestCallback((finished: boolean) => {
+ if (!finished || !prevVisible.current) {
+ return;
+ }
+
+ isShownRef.current = true;
+ focusFirstDOMNode(menuRef.current);
+ });
+
+ const handleHideAnimationFinished = useLatestCallback((finished: boolean) => {
+ if (!finished || prevVisible.current) {
+ return;
+ }
+
+ setMenuLayout({ width: 0, height: 0 });
+ setRendered(false);
+ isShownRef.current = false;
+ focusFirstDOMNode(anchorRef.current);
+ });
+
const show = React.useCallback(async () => {
const windowLayoutResult = Dimensions.get('window');
const [menuLayoutResult, anchorLayoutResult] = await Promise.all([
@@ -308,6 +340,10 @@ const Menu = ({
measureAnchorLayout(),
]);
+ if (!prevVisible.current) {
+ return;
+ }
+
// When visible is true for first render
// native views can be still not rendered and
// measureMenuLayout/measureAnchorLayout functions
@@ -344,45 +380,54 @@ const Menu = ({
});
attachListeners();
+
requestAnimationFrame(() => {
+ if (!prevVisible.current) {
+ return;
+ }
+
const { animation } = theme;
- Animated.parallel([
- Animated.timing(scaleAnimationRef.current, {
- toValue: { x: menuLayoutResult.width, y: menuLayoutResult.height },
- duration: ANIMATION_DURATION * animation.scale,
- easing: EASING,
- useNativeDriver: true,
- }),
- Animated.timing(opacityAnimationRef.current, {
- toValue: 1,
- duration: ANIMATION_DURATION * animation.scale,
- easing: EASING,
- useNativeDriver: true,
- }),
- ]).start(() => {
- focusFirstDOMNode(menuRef.current);
- prevRendered.current = true;
- });
+
+ const config = {
+ duration: ANIMATION_DURATION * animation.scale,
+ easing: EASING,
+ reduceMotion: ReduceMotion.Never,
+ };
+
+ scaleX.value = withTiming(menuLayoutResult.width, config);
+ scaleY.value = withTiming(menuLayoutResult.height, config);
+
+ opacity.value = withTiming(1, config, (finished) =>
+ scheduleOnRN(handleShowAnimationFinished, finished ?? false)
+ );
});
- }, [anchor, attachListeners, measureAnchorLayout, theme]);
+ }, [
+ anchor,
+ attachListeners,
+ handleShowAnimationFinished,
+ measureAnchorLayout,
+ opacity,
+ scaleX,
+ scaleY,
+ theme,
+ ]);
const hide = React.useCallback(() => {
removeListeners();
+ isShownRef.current = false;
const { animation } = theme;
- Animated.timing(opacityAnimationRef.current, {
- toValue: 0,
- duration: ANIMATION_DURATION * animation.scale,
- easing: EASING,
- useNativeDriver: true,
- }).start(() => {
- setMenuLayout({ width: 0, height: 0 });
- setRendered(false);
- prevRendered.current = false;
- focusFirstDOMNode(anchorRef.current);
- });
- }, [removeListeners, theme]);
+ opacity.value = withTiming(
+ 0,
+ {
+ duration: ANIMATION_DURATION * animation.scale,
+ easing: EASING,
+ reduceMotion: ReduceMotion.Never,
+ },
+ (finished) => scheduleOnRN(handleHideAnimationFinished, finished ?? false)
+ );
+ }, [handleHideAnimationFinished, opacity, removeListeners, theme]);
const updateVisibility = React.useCallback(
async (display: boolean) => {
@@ -390,7 +435,7 @@ const Menu = ({
// We need to do the same here so that the ref is up-to-date
await Promise.resolve();
- if (display && !prevRendered.current) {
+ if (display && !isShownRef.current) {
await show();
return;
}
@@ -403,8 +448,6 @@ const Menu = ({
);
React.useEffect(() => {
- const opacityAnimation = opacityAnimationRef.current;
- const scaleAnimation = scaleAnimationRef.current;
keyboardDidShowListenerRef.current = Keyboard.addListener(
'keyboardDidShow',
keyboardDidShow
@@ -418,26 +461,24 @@ const Menu = ({
removeListeners();
keyboardDidShowListenerRef.current?.remove();
keyboardDidHideListenerRef.current?.remove();
- scaleAnimation.removeAllListeners();
- opacityAnimation?.removeAllListeners();
};
}, [removeListeners, keyboardDidHide, keyboardDidShow]);
+ if (visible && !rendered) {
+ // Mount the Portal before attempting to show.
+ setRendered(true);
+ }
+
React.useEffect(() => {
if (prevVisible.current !== visible) {
prevVisible.current = visible;
- if (visible) {
- if (!rendered) {
- // Mount the Portal before attempting to show.
- setRendered(true);
- }
- } else {
+ if (!visible) {
// Keep the Portal mounted so the hide animation can finish.
void updateVisibility(false);
}
}
- }, [visible, rendered, updateVisibility]);
+ }, [visible, updateVisibility]);
React.useEffect(() => {
if (rendered && visible) {
@@ -452,7 +493,9 @@ const Menu = ({
});
// We need to translate menu while animating scale to imitate transform origin for scale animation
- const positionTransforms = [];
+ let startTranslateX = 0;
+ let startTranslateY = 0;
+
let leftTransformation = left;
let topTransformation =
!isCoordinate(anchorRef.current) && anchorPosition === 'bottom'
@@ -461,24 +504,14 @@ const Menu = ({
// Check if menu fits horizontally and if not align it to right.
if (left <= windowLayout.width - menuLayout.width - SCREEN_INDENT) {
- positionTransforms.push({
- translateX: scaleAnimationRef.current.x.interpolate({
- inputRange: [0, menuLayout.width],
- outputRange: [-(menuLayout.width / 2), 0],
- }),
- });
+ startTranslateX = -(menuLayout.width / 2);
// Check if menu position has enough space from left side
if (leftTransformation < SCREEN_INDENT) {
leftTransformation = SCREEN_INDENT;
}
} else {
- positionTransforms.push({
- translateX: scaleAnimationRef.current.x.interpolate({
- inputRange: [0, menuLayout.width],
- outputRange: [menuLayout.width / 2, 0],
- }),
- });
+ startTranslateX = menuLayout.width / 2;
leftTransformation += anchorLayout.width - menuLayout.width;
@@ -559,24 +592,14 @@ const Menu = ({
// And bottom side of the screen has more space than top side
topTransformation <= windowLayout.height - topTransformation)
) {
- positionTransforms.push({
- translateY: scaleAnimationRef.current.y.interpolate({
- inputRange: [0, menuLayout.height],
- outputRange: [-((scrollableMenuHeight || menuLayout.height) / 2), 0],
- }),
- });
+ startTranslateY = -((scrollableMenuHeight || menuLayout.height) / 2);
// Check if menu position has enough space from top side
if (topTransformation < SCREEN_INDENT) {
topTransformation = SCREEN_INDENT;
}
} else {
- positionTransforms.push({
- translateY: scaleAnimationRef.current.y.interpolate({
- inputRange: [0, menuLayout.height],
- outputRange: [(scrollableMenuHeight || menuLayout.height) / 2, 0],
- }),
- });
+ startTranslateY = (scrollableMenuHeight || menuLayout.height) / 2;
topTransformation +=
anchorLayout.height - (scrollableMenuHeight || menuLayout.height);
@@ -598,25 +621,38 @@ const Menu = ({
}
}
- const shadowMenuContainerStyle = {
- opacity: opacityAnimationRef.current,
+ const shadowMenuContainerStyle: ViewStyle = scrollableMenuHeight
+ ? { height: scrollableMenuHeight }
+ : {};
+
+ const positionTransformsStyle = useAnimatedStyle(() => {
+ const scaleXProgress = menuLayout.width
+ ? scaleX.value / menuLayout.width
+ : 0;
+
+ const scaleYProgress = menuLayout.height
+ ? scaleY.value / menuLayout.height
+ : 0;
+
+ return {
+ transform: [
+ { translateX: startTranslateX * (1 - scaleXProgress) },
+ { translateY: startTranslateY * (1 - scaleYProgress) },
+ ],
+ };
+ });
+
+ const shadowMenuAnimationStyle = useAnimatedStyle(() => ({
+ opacity: opacity.value,
transform: [
{
- scaleX: scaleAnimationRef.current.x.interpolate({
- inputRange: [0, menuLayout.width],
- outputRange: [0, 1],
- }),
+ scaleX: menuLayout.width ? scaleX.value / menuLayout.width : 0,
},
{
- scaleY: scaleAnimationRef.current.y.interpolate({
- inputRange: [0, menuLayout.height],
- outputRange: [0, 1],
- }),
+ scaleY: menuLayout.height ? scaleY.value / menuLayout.height : 0,
},
],
- borderRadius: theme.shapes.corner.extraSmall,
- ...(scrollableMenuHeight ? { height: scrollableMenuHeight } : {}),
- };
+ }));
const positionStyle = {
top: isCoordinate(anchor)
@@ -659,33 +695,36 @@ const Menu = ({
>
- {(scrollableMenuHeight && (
-
- {children}
-
- )) || {children} }
+
+ {(scrollableMenuHeight && (
+
+ {children}
+
+ )) || {children} }
+
@@ -703,8 +742,13 @@ const styles = StyleSheet.create({
},
shadowMenuContainer: {
opacity: 0,
+ },
+ menuContent: {
paddingVertical: 8,
},
+ fill: {
+ height: '100%',
+ },
pressableOverlay: {
...Platform.select({
web: {
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
index 3c66c885b3..22a3f7ba8e 100644
--- a/src/components/Modal.tsx
+++ b/src/components/Modal.tsx
@@ -1,17 +1,21 @@
import * as React from 'react';
-import { Animated, Easing, StyleSheet, Pressable, View } from 'react-native';
+import { StyleSheet, Pressable, View } from 'react-native';
import type { StyleProp, ViewStyle } from 'react-native';
+import Animated, {
+ cubicBezier,
+ type AnimatedStyle,
+} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import useLatestCallback from 'use-latest-callback';
import Surface from './Surface';
+import type { Props as SurfaceProps, SurfaceStyle } from './Surface';
import { useInternalTheme } from '../core/theming';
import { tokens } from '../theme/tokens';
-import type { ThemeProp } from '../types';
+import type { Elevation, ThemeProp } from '../types';
import { addEventListener } from '../utils/addEventListener';
import { BackHandler } from '../utils/BackHandler/BackHandler';
-import useAnimatedValue from '../utils/useAnimatedValue';
const scrimAlpha = tokens.md.sys.scrim.alpha;
@@ -41,9 +45,25 @@ export type Props = {
*/
children: React.ReactNode;
/**
- * Style for the content of the modal
+ * Style for the content of the modal.
+ *
+ * Background color and border radius should be specified via props instead:
+ * - `contentBackgroundColor`
+ * - `contentBorderRadius`
*/
- contentContainerStyle?: Animated.WithAnimatedValue>;
+ contentContainerStyle?: StyleProp;
+ /**
+ * Background color of the modal content. Defaults to transparent.
+ */
+ contentBackgroundColor?: SurfaceProps['backgroundColor'];
+ /**
+ * Border radius of the modal content.
+ */
+ contentBorderRadius?: SurfaceProps['borderRadius'];
+ /**
+ * Elevation level of the modal content. Defaults to level 1.
+ */
+ contentElevation?: Elevation;
/**
* Style for the wrapper of the modal.
* Use this prop to change the default wrapper style or to override safe area insets with marginTop and marginBottom.
@@ -77,12 +97,18 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
*
* const showModal = () => setVisible(true);
* const hideModal = () => setVisible(false);
- * const containerStyle = { backgroundColor: 'white', padding: 20 };
+ *
+ * const containerStyle = { padding: 20 };
*
* return (
*
*
- *
+ *
* Example Modal. Click outside this area to dismiss.
*
*
@@ -104,55 +130,46 @@ function Modal({
onDismiss = () => {},
children,
contentContainerStyle,
+ contentBackgroundColor = 'transparent',
+ contentBorderRadius,
+ contentElevation,
style,
theme: themeOverrides,
testID = 'modal',
}: Props) {
const theme = useInternalTheme(themeOverrides);
+
const onDismissCallback = useLatestCallback(onDismiss);
- const { scale } = theme.animation;
+
const { top, bottom } = useSafeAreaInsets();
- const opacity = useAnimatedValue(visible ? 1 : 0);
+
const [visibleInternal, setVisibleInternal] = React.useState(visible);
+ const [animatedVisible, setAnimatedVisible] = React.useState(visible);
- const showModalAnimation = React.useCallback(() => {
- Animated.timing(opacity, {
- toValue: 1,
- duration: scale * DEFAULT_DURATION,
- easing: Easing.out(Easing.cubic),
- useNativeDriver: true,
- }).start();
- }, [opacity, scale]);
-
- const hideModalAnimation = React.useCallback(() => {
- Animated.timing(opacity, {
- toValue: 0,
- duration: scale * DEFAULT_DURATION,
- easing: Easing.out(Easing.cubic),
- useNativeDriver: true,
- }).start(({ finished }) => {
- if (!finished) {
- return;
- }
+ if (visible && !visibleInternal) {
+ setVisibleInternal(true);
+ }
- setVisibleInternal(false);
- });
- }, [opacity, scale]);
+ const { scale } = theme.animation;
React.useEffect(() => {
- if (visibleInternal === visible) {
- return;
- }
+ const timeout = setTimeout(() => setAnimatedVisible(visible), 0);
- if (!visibleInternal && visible) {
- setVisibleInternal(true);
- return showModalAnimation();
- }
+ return () => clearTimeout(timeout);
+ }, [visible]);
- if (visibleInternal && !visible) {
- return hideModalAnimation();
+ React.useEffect(() => {
+ if (visible || !visibleInternal) {
+ return undefined;
}
- }, [visible, showModalAnimation, hideModalAnimation, visibleInternal]);
+
+ const timeout = setTimeout(
+ () => setVisibleInternal(false),
+ scale * DEFAULT_DURATION
+ );
+
+ return () => clearTimeout(timeout);
+ }, [scale, visible, visibleInternal]);
React.useEffect(() => {
if (!visible) {
@@ -172,10 +189,33 @@ function Modal({
'hardwareBackPress',
onHardwareBackPress
);
+
return () => subscription.remove();
}, [dismissable, dismissableBackButton, onDismissCallback, visible]);
- if (!visibleInternal) {
+ const transitionTimingFunction = cubicBezier(1 / 3, 1, 2 / 3, 1);
+
+ const backdropTransitionStyle: AnimatedStyle = {
+ transitionDuration: scale * DEFAULT_DURATION,
+ transitionProperty: 'opacity',
+ transitionTimingFunction,
+ };
+
+ const contentTransitionStyle: AnimatedStyle = {
+ transitionProperty: 'opacity',
+ transitionTimingFunction,
+ };
+
+ const backdropStyle: AnimatedStyle = {
+ backgroundColor: theme.colors.scrim,
+ opacity: animatedVisible ? scrimAlpha : 0,
+ };
+
+ const contentStyle: AnimatedStyle = {
+ opacity: animatedVisible ? 1 : 0,
+ };
+
+ if (!visible && !visibleInternal) {
return null;
}
@@ -194,16 +234,7 @@ function Modal({
disabled={!dismissable}
onPress={dismissable ? onDismissCallback : undefined}
importantForAccessibility="no"
- style={[
- styles.backdrop,
- {
- backgroundColor: theme.colors.scrim,
- opacity: opacity.interpolate({
- inputRange: [0, 1],
- outputRange: [0, scrimAlpha],
- }),
- },
- ]}
+ style={[styles.backdrop, backdropStyle, backdropTransitionStyle]}
testID={`${testID}-backdrop`}
/>
{children}
@@ -238,9 +277,7 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFill,
justifyContent: 'center',
},
- // eslint-disable-next-line react-native/no-color-literals
content: {
- backgroundColor: 'transparent',
justifyContent: 'center',
},
});
diff --git a/src/components/Searchbar.tsx b/src/components/Searchbar.tsx
index b0819b5710..8d7e038c31 100644
--- a/src/components/Searchbar.tsx
+++ b/src/components/Searchbar.tsx
@@ -1,12 +1,11 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, TextInput, View } from 'react-native';
+import { Platform, StyleSheet, TextInput, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
StyleProp,
TextInputProps,
TextStyle,
- ViewStyle,
} from 'react-native';
import ActivityIndicator from './ActivityIndicator';
@@ -15,10 +14,11 @@ import type { IconSource } from './Icon';
import IconButton from './IconButton/IconButton';
import MaterialCommunityIcon from './MaterialCommunityIcon';
import Surface from './Surface';
+import type { SurfaceStyle } from './Surface';
import { useLocale } from '../core/locale';
import { useInternalTheme } from '../core/theming';
import { cornerNone } from '../theme/tokens/sys/shape';
-import type { ThemeProp } from '../types';
+import type { Elevation, ThemeProp } from '../types';
interface Style {
marginRight: number;
@@ -111,12 +111,12 @@ export type Props = TextInputProps & {
* @supported Available in v5.x with theme version 3
* Changes Searchbar shadow and background on iOS and Android.
*/
- elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
+ elevation?: Elevation;
/**
* Set style of the TextInput component inside the searchbar
*/
inputStyle?: StyleProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
/**
* Custom flag for replacing clear button with activity indicator.
*/
@@ -188,8 +188,10 @@ const Searchbar = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
+
const { direction } = useLocale();
const { colors, fonts } = theme;
+
const root = React.useRef(null);
React.useImperativeHandle(ref, () => ({
@@ -230,18 +232,11 @@ const Searchbar = ({
return (
, 'mode'> & {
+export type Props = Omit & {
/**
* Whether the Snackbar is currently visible.
*/
@@ -58,7 +68,7 @@ export type Props = $Omit, 'mode'> & {
* @supported Available in v5.x with theme version 3
* Changes Snackbar shadow and background on iOS and Android.
*/
- elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
+ elevation?: Elevation;
/**
* Specifies the largest possible scale a text font can reach.
*/
@@ -71,7 +81,7 @@ export type Props = $Omit, 'mode'> & {
* Style for the content of the snackbar
*/
contentStyle?: StyleProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
ref?: React.RefObject;
/**
* @optional
@@ -152,82 +162,83 @@ const Snackbar = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
+
const { direction } = useLocale();
+
const { bottom, right, left } = useSafeAreaInsets();
- const { current: opacity } = React.useRef(
- new Animated.Value(0.0)
- );
+ const opacity = useSharedValue(0);
+
const hideTimeout = React.useRef | undefined>(
undefined
);
+ const isMounted = React.useRef(true);
const [hidden, setHidden] = React.useState(!visible);
const { scale } = theme.animation;
- const animateShow = useLatestCallback(() => {
- if (hideTimeout.current) clearTimeout(hideTimeout.current);
-
- Animated.timing(opacity, {
- toValue: 1,
- duration: 200 * scale,
- easing: Easing.out(Easing.ease),
- useNativeDriver: true,
- }).start(({ finished }) => {
- if (finished) {
- const isInfinity =
- duration === Number.POSITIVE_INFINITY ||
- duration === Number.NEGATIVE_INFINITY;
-
- if (!isInfinity) {
- hideTimeout.current = setTimeout(onDismiss, duration);
- }
- }
- });
- });
-
- const handleOnVisible = useLatestCallback(() => {
- // show
+ if (visible && hidden) {
setHidden(false);
+ }
+
+ const handleShowAnimationFinished = useLatestCallback((finished: boolean) => {
+ if (!finished || !visible || !isMounted.current) {
+ return;
+ }
+
+ const isInfinity =
+ duration === Number.POSITIVE_INFINITY ||
+ duration === Number.NEGATIVE_INFINITY;
+
+ if (!isInfinity) {
+ hideTimeout.current = setTimeout(onDismiss, duration);
+ }
});
- const handleOnHidden = useLatestCallback(() => {
- // hide
+ React.useEffect(() => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
+ hideTimeout.current = undefined;
}
- Animated.timing(opacity, {
- toValue: 0,
- duration: 100 * scale,
- useNativeDriver: true,
- }).start(({ finished }) => {
- if (finished) {
- setHidden(true);
+ opacity.value = withTiming(
+ visible ? 1 : 0,
+ {
+ duration: (visible ? 200 : 100) * scale,
+ easing: visible ? Easing.out(Easing.ease) : Easing.inOut(Easing.ease),
+ reduceMotion: ReduceMotion.Never,
+ },
+ (finished) => {
+ if (visible) {
+ scheduleOnRN(handleShowAnimationFinished, finished ?? false);
+ } else if (finished) {
+ scheduleOnRN(setHidden, true);
+ }
}
- });
- });
+ );
+ }, [handleShowAnimationFinished, opacity, scale, visible]);
React.useEffect(() => {
- if (!hidden) {
- animateShow();
- }
- }, [animateShow, hidden]);
+ isMounted.current = true;
- React.useEffect(() => {
return () => {
- if (hideTimeout.current) clearTimeout(hideTimeout.current);
+ isMounted.current = false;
+
+ if (hideTimeout.current) {
+ clearTimeout(hideTimeout.current);
+ }
};
}, []);
- React.useLayoutEffect(() => {
- if (visible) {
- handleOnVisible();
- } else {
- handleOnHidden();
- }
- }, [visible, handleOnVisible, handleOnHidden]);
+ const animatedStyle = useAnimatedStyle(() => ({
+ opacity: opacity.value,
+ transform: [
+ {
+ scale: visible ? interpolate(opacity.value, [0, 1], [0.9, 1]) : 1,
+ },
+ ],
+ }));
const { colors } = theme;
@@ -255,26 +266,21 @@ const Snackbar = ({
paddingHorizontal: Math.max(left, right),
};
- const renderChildrenWithWrapper = () => {
- if (typeof children === 'string') {
- return (
-
- {children}
-
- );
- }
-
- return (
+ const content =
+ typeof children === 'string' ? (
+
+ {children}
+
+ ) : (
{/* View is added to allow multiple lines support for Text component as children */}
{children}
);
- };
return (
- {renderChildrenWithWrapper()}
+ {content}
{(action || isIconButton) && (
{action ? (
@@ -385,8 +373,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
margin: 8,
- borderRadius: 4,
minHeight: 48,
+ pointerEvents: 'box-none',
},
content: {
marginHorizontal: 16,
diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx
index b0b0f0c4aa..fa84f49051 100644
--- a/src/components/Surface.tsx
+++ b/src/components/Surface.tsx
@@ -1,205 +1,147 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, View } from 'react-native';
-import type {
- ColorValue,
- ShadowStyleIOS,
- StyleProp,
- ViewProps,
- ViewStyle,
-} from 'react-native';
+import { Platform, StyleSheet, View } from 'react-native';
+import type { ColorValue, StyleProp, ViewProps, ViewStyle } from 'react-native';
+
+import Animated, {
+ cubicBezier,
+ isSharedValue,
+ type AnimatedStyle,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
import { useInternalTheme } from '../core/theming';
-import {
- androidElevationLevels,
- elevationInputRange,
- shadow,
- shadowLayers,
-} from '../theme/tokens/sys/elevation';
+import { androidElevationLevels, shadow } from '../theme/tokens/sys/elevation';
import type { Elevation, ThemeProp } from '../types';
-import { isAnimatedValue } from '../utils/animations';
-import { splitStyles } from '../utils/splitStyles';
-type SurfaceElevation = Elevation | Animated.Value;
+type AnimatedStyleProp = Extract<
+ AnimatedStyle>>,
+ Record
+>[Key];
+
+type BorderRadius = AnimatedStyleProp<'borderRadius'>;
-export type Props = Omit & {
+type SurfaceVisualProps = {
+ /**
+ * Background color of the Surface. Overrides the color derived from
+ * `elevation`.
+ */
+ backgroundColor?: ColorValue;
+ /**
+ * Radius of every corner of the Surface.
+ */
+ borderRadius?: BorderRadius;
+ /**
+ * Radius of the bottom-end corner of the Surface.
+ */
+ borderBottomEndRadius?: BorderRadius;
+ /**
+ * Radius of the bottom-left corner of the Surface.
+ */
+ borderBottomLeftRadius?: BorderRadius;
/**
- * Content of the `Surface`.
+ * Radius of the bottom-right corner of the Surface.
*/
- children: React.ReactNode;
- style?: Animated.WithAnimatedValue>;
+ borderBottomRightRadius?: BorderRadius;
/**
- * @supported Available in v5.x with theme version 3
- * Changes shadows and background on iOS and Android.
- * Used to create UI hierarchy between components.
- *
- * Note: If `mode` is set to `flat`, Surface doesn't have a shadow.
- *
- * Note: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`.
- * It's no longer supported with theme version 3 and you should use `elevation` property instead.
+ * Radius of the bottom-start corner of the Surface.
*/
- elevation?: SurfaceElevation;
+ borderBottomStartRadius?: BorderRadius;
/**
- * @supported Available in v5.x with theme version 3
- * Mode of the Surface.
- * - `elevated` - Surface with a shadow and background color corresponding to set `elevation` value.
- * - `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value.
+ * Radius of the end-end corner of the Surface.
*/
- mode?: 'flat' | 'elevated';
+ borderEndEndRadius?: BorderRadius;
/**
- * @optional
+ * Radius of the end-start corner of the Surface.
*/
- theme?: ThemeProp;
+ borderEndStartRadius?: BorderRadius;
/**
- * TestID used for testing purposes
+ * Radius of the start-end corner of the Surface.
*/
- testID?: string;
- ref?: React.Ref;
+ borderStartEndRadius?: BorderRadius;
/**
- * @internal
+ * Radius of the start-start corner of the Surface.
*/
- container?: boolean;
+ borderStartStartRadius?: BorderRadius;
+ /**
+ * Radius of the top-end corner of the Surface.
+ */
+ borderTopEndRadius?: BorderRadius;
+ /**
+ * Radius of the top-left corner of the Surface.
+ */
+ borderTopLeftRadius?: BorderRadius;
+ /**
+ * Radius of the top-right corner of the Surface.
+ */
+ borderTopRightRadius?: BorderRadius;
+ /**
+ * Radius of the top-start corner of the Surface.
+ */
+ borderTopStartRadius?: BorderRadius;
+ /**
+ * Corner curve of the Surface on iOS.
+ */
+ borderCurve?: ViewStyle['borderCurve'];
};
-const outerLayerStyleProperties: (keyof ViewStyle)[] = [
- 'position',
- 'alignSelf',
- 'top',
- 'right',
- 'bottom',
- 'left',
- 'start',
- 'end',
- 'flex',
- 'flexShrink',
- 'flexGrow',
- 'width',
- 'height',
- 'transform',
- 'opacity',
-];
+export type SurfaceStyle = AnimatedStyle<
+ Omit
+>;
-function getStyleForShadowLayer(
- elevation: SurfaceElevation,
- layer: 0 | 1,
- shadowColor: ColorValue
-): Animated.WithAnimatedValue {
- if (isAnimatedValue(elevation)) {
- return {
- shadowColor,
- shadowOpacity: elevation.interpolate({
- inputRange: [0, 1],
- outputRange: [0, shadowLayers[layer].shadowOpacity],
- extrapolate: 'clamp',
- }),
- shadowOffset: {
- width: 0,
- height: elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: shadowLayers[layer].height,
- }),
- },
- shadowRadius: elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: shadowLayers[layer].shadowRadius,
- }),
- };
- }
-
- return {
- shadowColor,
- shadowOpacity: elevation ? shadowLayers[layer].shadowOpacity : 0,
- shadowOffset: {
- width: 0,
- height: shadowLayers[layer].height[elevation],
- },
- shadowRadius: shadowLayers[layer].shadowRadius[elevation],
+export type Props = Omit &
+ SurfaceVisualProps & {
+ /**
+ * Duration of the background, elevation, and shadow transitions in
+ * milliseconds.
+ */
+ transitionDuration?: number;
+ /**
+ * Style of the Surface.
+ *
+ * This doesn't support all View style properties:
+ * - Background color and border radius should be specified via props instead.
+ * - `overflow: 'hidden'` is not supported with `elevation` as it can clip the shadow.
+ * To achieve the same effect, wrap the content in a child View with the overflow style.
+ */
+ style?: StyleProp;
+ /**
+ * @supported Available in v5.x with theme version 3
+ * Changes shadows and background on iOS and Android.
+ * Used to create UI hierarchy between components.
+ *
+ * Note: If `mode` is set to `flat`, Surface doesn't have a shadow.
+ *
+ * Note: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`.
+ * It's no longer supported with theme version 3 and you should use `elevation` property instead.
+ */
+ elevation?: Elevation;
+ /**
+ * @supported Available in v5.x with theme version 3
+ * Mode of the Surface.
+ * - `elevated` - Surface with a shadow and background color corresponding to set `elevation` value.
+ * - `flat` - Surface without a shadow, with the background color corresponding to set `elevation` value.
+ */
+ mode?: 'flat' | 'elevated';
+ /**
+ * @optional
+ */
+ theme?: ThemeProp;
+ /**
+ * Content of the `Surface`.
+ */
+ children: React.ReactNode;
+ /**
+ * TestID used for testing purposes
+ */
+ testID?: string;
+ ref?: React.Ref;
};
-}
-
-type SurfaceIOSProps = Omit & {
- elevation: SurfaceElevation;
- backgroundColor?:
- | ColorValue
- | Animated.AnimatedInterpolation;
- shadowColor: ColorValue;
-};
-
-const SurfaceIOS = ({
- elevation,
- style,
- backgroundColor,
- shadowColor,
- testID,
- children,
- mode = 'elevated',
- container,
- ref,
- ...props
-}: SurfaceIOSProps) => {
- const [outerLayerViewStyles, innerLayerViewStyles] = React.useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- const flattenedStyles = (StyleSheet.flatten(style) || {}) as ViewStyle;
-
- const [filteredStyles, outerLayerStyles, borderRadiusStyles] = splitStyles(
- flattenedStyles,
- (style) =>
- outerLayerStyleProperties.includes(style) || style.startsWith('margin'),
- (style) => style.startsWith('border') && style.endsWith('Radius')
- );
-
- if (
- process.env.NODE_ENV !== 'production' &&
- filteredStyles.overflow === 'hidden' &&
- elevation !== 0
- ) {
- console.warn(
- 'When setting overflow to hidden on Surface the shadow will not be displayed correctly. Wrap the content of your component in a separate View with the overflow style.'
- );
- }
-
- const bgColor = flattenedStyles.backgroundColor || backgroundColor;
-
- const isElevated = mode === 'elevated';
-
- const outerLayerViewStyles = {
- ...(isElevated && getStyleForShadowLayer(elevation, 0, shadowColor)),
- ...outerLayerStyles,
- ...borderRadiusStyles,
- backgroundColor: bgColor,
- };
-
- const innerLayerViewStyles = {
- ...(isElevated && getStyleForShadowLayer(elevation, 1, shadowColor)),
- ...filteredStyles,
- ...borderRadiusStyles,
- flex:
- flattenedStyles.height || (!container && flattenedStyles.flex)
- ? 1
- : undefined,
- backgroundColor: bgColor,
- };
-
- return [outerLayerViewStyles, innerLayerViewStyles];
- }, [style, elevation, backgroundColor, shadowColor, mode, container]);
-
- return (
-
-
- {children}
-
-
- );
-};
/**
* Surface is a basic container that can give depth to an element with elevation shadow.
- * On dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.
- * See [Dark Theme](https://callstack.github.io/react-native-paper/docs/guides/theming#dark-theme) for more information.
- * Overlay and shadow can be applied by specifying the `elevation` property both on Android and iOS.
+ *
+ * On Android, Surface uses the native `elevation` style,
+ * and falls back to shadows that approximate the elevation on other platforms.
*
* ## Usage
* ```js
@@ -208,7 +150,7 @@ const SurfaceIOS = ({
* import { StyleSheet } from 'react-native';
*
* const MyComponent = () => (
- *
+ *
* Surface
*
* );
@@ -217,9 +159,9 @@ const SurfaceIOS = ({
*
* const styles = StyleSheet.create({
* surface: {
- * padding: 8,
* height: 80,
* width: 80,
+ * padding: 8,
* alignItems: 'center',
* justifyContent: 'center',
* },
@@ -231,44 +173,92 @@ const Surface = ({
children,
theme: overridenTheme,
style,
- testID = 'surface',
+ backgroundColor: customBackgroundColor,
+ borderRadius,
+ borderBottomEndRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ borderBottomStartRadius,
+ borderEndEndRadius,
+ borderEndStartRadius,
+ borderStartEndRadius,
+ borderStartStartRadius,
+ borderTopEndRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderTopStartRadius,
+ borderCurve = 'continuous',
+ testID,
mode = 'elevated',
+ transitionDuration: customTransitionDuration,
ref,
- ...props
+ ...rest
}: Props) => {
const theme = useInternalTheme(overridenTheme);
const { colors } = theme;
- const backgroundColor = (() => {
- if (isAnimatedValue(elevation)) {
- return elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: elevationInputRange.map((elevation) => {
- return colors.elevation?.[`level${elevation}`];
- }),
- });
- }
+ const backgroundColor =
+ customBackgroundColor ?? colors.elevation?.[`level${elevation}`];
- return colors.elevation?.[`level${elevation}`];
- })();
+ const backgroundStyle = { backgroundColor };
+
+ const shapeProps = {
+ borderRadius,
+ borderBottomEndRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ borderBottomStartRadius,
+ borderEndEndRadius,
+ borderEndStartRadius,
+ borderStartEndRadius,
+ borderStartStartRadius,
+ borderTopEndRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderTopStartRadius,
+ borderCurve,
+ };
+
+ // Reanimated styles can't be shared between different views
+ // So we need to create two separate styles for the surface and shadow layers
+ const visualStyle = useSurfaceVisualStyle(shapeProps);
+ const shadowVisualStyle = useSurfaceVisualStyle(shapeProps);
const isElevated = mode === 'elevated';
+ const transitionDuration =
+ customTransitionDuration ??
+ theme.motion.duration.short3 * theme.animation.scale;
+ const transitionDurationStyle: AnimatedStyle = {
+ transitionDuration,
+ };
+ const transitionTimingFunction = cubicBezier(...theme.motion.easing.standard);
+ const transitionProperty =
+ // FIXME: Reanimated can't animate PlatformColor and DynamicColorIOS
+ typeof backgroundColor === 'string' ? ['backgroundColor' as const] : [];
+
if (Platform.OS === 'web') {
- const { pointerEvents = 'auto' } = props;
+ const [elevationShadow] = shadow(elevation, theme.colors.shadow);
+
+ const transitionStyle: AnimatedStyle = {
+ transitionTimingFunction,
+ transitionProperty: [...transitionProperty, 'boxShadow'],
+ };
+
return (
{children}
@@ -277,40 +267,25 @@ const Surface = ({
}
if (Platform.OS === 'android') {
- const getElevationAndroid = () => {
- if (isAnimatedValue(elevation)) {
- return elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: androidElevationLevels,
- });
- }
+ const elevationAndroid = androidElevationLevels[elevation];
- return androidElevationLevels[elevation];
+ const transitionStyle: AnimatedStyle = {
+ transitionTimingFunction,
+ transitionProperty: [...transitionProperty, 'elevation'],
};
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- const { margin, padding, transform, borderRadius } = (StyleSheet.flatten(
- style
- ) || {}) as ViewStyle;
-
- const outerLayerStyles = { margin, padding, transform, borderRadius };
- const sharedStyle = [{ backgroundColor }, style];
-
return (
{children}
@@ -318,20 +293,97 @@ const Surface = ({
);
}
+ const [spotShadow, ambientShadow] = shadow(elevation, theme.colors.shadow);
+
+ const transitionStyle: AnimatedStyle = {
+ transitionTimingFunction,
+ transitionProperty: [
+ ...transitionProperty,
+ 'shadowOpacity',
+ 'shadowOffset',
+ 'shadowRadius',
+ ],
+ };
+
return (
-
+ {isElevated ? (
+
+ ) : null}
{children}
-
+
);
};
+const useSurfaceVisualStyle = ({
+ borderRadius,
+ borderBottomEndRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ borderBottomStartRadius,
+ borderEndEndRadius,
+ borderEndStartRadius,
+ borderStartEndRadius,
+ borderStartStartRadius,
+ borderTopEndRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderTopStartRadius,
+ borderCurve,
+}: Omit) =>
+ useAnimatedStyle(() =>
+ Object.fromEntries(
+ Object.entries({
+ borderRadius,
+ borderBottomEndRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ borderBottomStartRadius,
+ borderEndEndRadius,
+ borderEndStartRadius,
+ borderStartEndRadius,
+ borderStartStartRadius,
+ borderTopEndRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderTopStartRadius,
+ borderCurve,
+ }).map(([property, value]) => [
+ property,
+ isSharedValue(value) ? value.value : value,
+ ])
+ )
+ );
+
+const styles = StyleSheet.create({
+ container: {
+ pointerEvents: 'auto',
+ },
+ shadow: {
+ pointerEvents: 'none',
+ },
+});
+
export default Surface;
diff --git a/src/components/ToggleButton/ToggleButton.tsx b/src/components/ToggleButton/ToggleButton.tsx
index 22598c16b2..50bb50547e 100644
--- a/src/components/ToggleButton/ToggleButton.tsx
+++ b/src/components/ToggleButton/ToggleButton.tsx
@@ -1,7 +1,9 @@
import * as React from 'react';
-import { StyleSheet, View, Animated } from 'react-native';
+import { StyleSheet, View } from 'react-native';
import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
import { ToggleButtonGroupContext } from './ToggleButtonGroup';
import { getToggleButtonColor } from './utils';
import { useInternalTheme } from '../../core/theming';
@@ -42,7 +44,7 @@ export type Props = {
* Status of button.
*/
status?: 'checked' | 'unchecked';
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp>;
/**
* @optional
*/
diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx
index 27cd573f9d..5bba9563f4 100644
--- a/src/components/__tests__/Appbar/Appbar.test.tsx
+++ b/src/components/__tests__/Appbar/Appbar.test.tsx
@@ -1,7 +1,4 @@
-import { Animated } from 'react-native';
-
-import { describe, expect, it, jest } from '@jest/globals';
-import { act } from '@testing-library/react-native';
+import { describe, expect, it } from '@jest/globals';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { getTheme } from '../../../core/theming';
@@ -280,176 +277,53 @@ describe('AppbarContent', () => {
});
describe('getAppbarColors', () => {
- const elevation = 4;
+ const elevated = true;
const customBackground = 'aquamarine';
it('should return custom color no matter what is the theme version', () => {
expect(
- getAppbarBackgroundColor(getTheme(), elevation, customBackground)
+ getAppbarBackgroundColor(getTheme(), elevated, customBackground)
).toBe(customBackground);
});
- it('should return v3 light color if theme version is 3', () => {
- expect(getAppbarBackgroundColor(getTheme(), elevation)).toBe(
- tokens.md.ref.palette.neutral98
+ it('returns the light surface container color for an elevated appbar', () => {
+ expect(getAppbarBackgroundColor(getTheme(), elevated)).toBe(
+ tokens.md.ref.palette.neutral94
);
});
- it('should return v3 dark color if theme version is 3', () => {
- expect(getAppbarBackgroundColor(getTheme(true), elevation)).toBe(
- tokens.md.ref.palette.neutral6
+ it('returns the dark surface container color for an elevated appbar', () => {
+ expect(getAppbarBackgroundColor(getTheme(true), elevated)).toBe(
+ tokens.md.ref.palette.neutral12
);
});
});
-describe('animated value changes correctly', () => {
- it('appbar animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
-
-
+describe('getAppbarBorders', () => {
+ const borderStyles = {
+ borderRadius: 1,
+ borderBottomEndRadius: 2,
+ borderBottomStartRadius: 3,
+ borderEndEndRadius: 4,
+ borderEndStartRadius: 5,
+ borderStartEndRadius: 6,
+ borderStartStartRadius: 7,
+ borderTopEndRadius: 8,
+ borderTopStartRadius: 9,
+ borderTopLeftRadius: 10,
+ borderTopRightRadius: 11,
+ borderBottomRightRadius: 12,
+ borderBottomLeftRadius: 13,
+ borderCurve: 'continuous' as const,
+ };
+
+ it('returns every border style and excludes unrelated styles', () => {
+ expect(getAppbarBorders({ ...borderStyles, height: 60, top: 13 })).toEqual(
+ borderStyles
);
- expect(screen.getByTestId('appbar-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
-
- expect(screen.getByTestId('appbar-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
- });
-
- it('action animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
-
-
- );
- expect(
- screen.getByTestId('appbar-action-container-outer-layer')
- ).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
-
- expect(
- screen.getByTestId('appbar-action-container-outer-layer')
- ).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
});
- it('back action animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
-
-
- );
- expect(
- screen.getByTestId('appbar-back-action-container-outer-layer')
- ).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
-
- expect(
- screen.getByTestId('appbar-back-action-container-outer-layer')
- ).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
- });
-
- it('header animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
-
- {null}
-
-
- );
- expect(screen.getByTestId('appbar-header-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
-
- expect(screen.getByTestId('appbar-header-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
- });
-
- it('header bottom border radius applied correctly', async () => {
- const style = { borderBottomLeftRadius: 16, borderBottomRightRadius: 16 };
-
- await render(
-
-
- {null}
-
-
- );
- expect(screen.getByTestId('appbar-header-root-layer')).toHaveStyle(style);
- });
-
- describe('getAppbarBorders', () => {
- const style = { borderRadius: 10, height: 60, top: 13 };
-
- it('should return only border radius styles', () => {
- expect(getAppbarBorders(style)).toEqual({ borderRadius: 10 });
- });
-
- it('should return empty object if no borders are passed', () => {
- const style = { height: 60, top: 13 };
- expect(getAppbarBorders(style)).toEqual({});
- });
+ it('returns an empty object when no border styles are passed', () => {
+ expect(getAppbarBorders({ height: 60, top: 13 })).toEqual({});
});
});
diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
index a5d9d95766..30aaf4e66d 100644
--- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
+++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
@@ -4,33 +4,43 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
-
+
-
+
-
+
+
+
-
-
-
-
- magnify
-
-
-
-
+ ],
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ magnify
+
-
+
+
+
+
+
-
-
-
-
- close
-
-
-
-
+ ],
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ close
+
@@ -420,33 +488,43 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
-
-
+
+
+
-
-
+
+
-
-
-
-
-
+ }
+ />
-
+
+
-
- Examples
-
-
-
+ Examples
+
+
+
+
-
-
-
- menu
-
-
-
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ menu
+
diff --git a/src/components/__tests__/Banner.test.tsx b/src/components/__tests__/Banner.test.tsx
index 80bd3e9017..5cbd07b387 100644
--- a/src/components/__tests__/Banner.test.tsx
+++ b/src/components/__tests__/Banner.test.tsx
@@ -1,4 +1,4 @@
-import { Animated, Image } from 'react-native';
+import { Image } from 'react-native';
import {
afterAll,
@@ -11,7 +11,7 @@ import {
} from '@jest/globals';
import { act } from '@testing-library/react-native';
-import { render, screen } from '../../test-utils';
+import { render } from '../../test-utils';
import Banner from '../Banner';
it('renders hidden banner, without action buttons and without image', async () => {
@@ -356,34 +356,4 @@ describe('animations', () => {
expect(nextHideCallback).toHaveBeenCalledTimes(1);
});
});
-
- it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- Banner
-
- );
- expect(screen.getByTestId('banner-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.runAllTimers();
- });
-
- expect(screen.getByTestId('banner-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
- });
});
diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx
index 21e495742a..e4a275921f 100644
--- a/src/components/__tests__/BottomNavigation.test.tsx
+++ b/src/components/__tests__/BottomNavigation.test.tsx
@@ -1,4 +1,12 @@
-import { Animated, Easing, Platform, StyleSheet, Text } from 'react-native';
+import {
+ Animated,
+ Easing,
+ Keyboard,
+ Platform,
+ StyleSheet,
+ Text,
+} from 'react-native';
+import type { KeyboardEvent } from 'react-native';
import { describe, expect, it, jest } from '@jest/globals';
import { act, fireEvent, userEvent } from '@testing-library/react-native';
@@ -431,6 +439,60 @@ it('renders custom background color passed to barStyle property', async () => {
expect(wrapper).toHaveStyle({ backgroundColor: Palette.error60 });
});
+it('uses the rendered bar height when hiding it for the keyboard', async () => {
+ let handleKeyboardShow: ((event: KeyboardEvent) => void) | undefined;
+ const addKeyboardListener = Keyboard.addListener.bind(Keyboard);
+ const keyboardListenerSpy = jest
+ .spyOn(Keyboard, 'addListener')
+ .mockImplementation((event, listener) => {
+ if (event.endsWith('Show')) {
+ handleKeyboardShow = listener;
+ }
+
+ return addKeyboardListener(event, listener);
+ });
+
+ await render(
+
+ );
+
+ const navigation = screen.getByTestId('bottom-navigation');
+
+ await fireEvent(navigation, 'layout', {
+ nativeEvent: {
+ layout: { height: 72, width: 360 },
+ },
+ });
+
+ await act(() => {
+ handleKeyboardShow?.({
+ duration: 0,
+ easing: 'keyboard',
+ endCoordinates: {
+ screenX: 0,
+ screenY: 500,
+ width: 360,
+ height: 300,
+ },
+ });
+ jest.runAllTimers();
+ });
+
+ expect(navigation).toHaveStyle({
+ height: 96,
+ position: 'absolute',
+ transform: [{ translateY: 72 }],
+ });
+
+ keyboardListenerSpy.mockRestore();
+});
+
it('renders a single tab', async () => {
await render(
{
);
});
-it('barStyle animated value changes correctly', async () => {
+it('supports animated styles in bar', async () => {
const value = new Animated.Value(1);
await render(
- {}}
- renderScene={renderScene}
- testID={'bottom-navigation'}
- barStyle={[{ transform: [{ scale: value }] }]}
+ onTabPress={jest.fn()}
+ testID="bottom-navigation"
+ style={[{ transform: [{ scale: value }] }]}
/>
);
- expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({
+
+ expect(screen.getByTestId('bottom-navigation')).toHaveStyle({
transform: [{ scale: 1 }],
});
@@ -614,7 +676,8 @@ it('barStyle animated value changes correctly', async () => {
await act(() => {
jest.advanceTimersByTime(200);
});
- expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({
+
+ expect(screen.getByTestId('bottom-navigation')).toHaveStyle({
transform: [{ scale: 1.5 }],
});
});
diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx
index 4da466837a..be3d573d29 100644
--- a/src/components/__tests__/Button.test.tsx
+++ b/src/components/__tests__/Button.test.tsx
@@ -1,7 +1,7 @@
-import { Animated, StyleSheet } from 'react-native';
+import { StyleSheet } from 'react-native';
import { describe, expect, it, jest } from '@jest/globals';
-import { act, userEvent } from '@testing-library/react-native';
+import { userEvent } from '@testing-library/react-native';
import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
@@ -16,15 +16,6 @@ const styles = StyleSheet.create({
flexing: {
flexDirection: 'row-reverse',
},
- customRadius: {
- borderTopLeftRadius: 16,
- borderTopRightRadius: 0,
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 16,
- },
- noRadius: {
- borderRadius: 0,
- },
});
it('renders text button by default', async () => {
@@ -155,54 +146,6 @@ it('renders button with an accessibility hint', async () => {
expect(tree).toMatchSnapshot();
});
-it('renders button with custom border radius', async () => {
- await render(
-
- Custom radius
-
- );
-
- expect(screen.getByTestId('custom-radius-container')).toHaveStyle(
- styles.customRadius
- );
- expect(screen.getByTestId('custom-radius')).toHaveStyle(styles.customRadius);
-});
-
-it('renders outlined button with custom border radius', async () => {
- await render(
-
- Custom radius
-
- );
-
- expect(screen.getByTestId('custom-radius-container')).toHaveStyle(
- styles.customRadius
- );
- expect(screen.getByTestId('custom-radius')).toHaveStyle({
- borderTopLeftRadius: 15, // styles.customRadius - 1px outline
- borderTopRightRadius: 0,
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 15, // styles.customRadius - 1px outline
- });
-});
-
-it('renders button without border radius', async () => {
- await render(
-
- Custom radius
-
- );
-
- expect(screen.getByTestId('custom-radius-container')).toHaveStyle(
- styles.noRadius
- );
- expect(screen.getByTestId('custom-radius')).toHaveStyle(styles.noRadius);
-});
-
it('should execute onPressIn', async () => {
const onPressInMock = jest.fn();
const onPress = jest.fn();
@@ -709,33 +652,3 @@ describe('getButtonColors - border width', () => {
})
);
});
-
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- Compact button
-
- );
- expect(screen.getByTestId('button-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('button-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx
index d9e91d9d73..74637a8a53 100644
--- a/src/components/__tests__/Card/Card.test.tsx
+++ b/src/components/__tests__/Card/Card.test.tsx
@@ -1,7 +1,6 @@
-import { Animated, StyleSheet, Text } from 'react-native';
+import { Platform, StyleSheet, Text } from 'react-native';
-import { describe, expect, it, jest } from '@jest/globals';
-import { act } from '@testing-library/react-native';
+import { afterEach, describe, expect, it, jest } from '@jest/globals';
import { getTheme } from '../../../core/theming';
import { render, screen } from '../../../test-utils';
@@ -11,9 +10,6 @@ import Card from '../../Card/Card';
import { getCardColors, getCardCoverStyle } from '../../Card/utils';
const styles = StyleSheet.create({
- customBorderRadius: {
- borderRadius: 32,
- },
customCoverRadius: {
borderTopLeftRadius: 4,
borderTopRightRadius: 8,
@@ -25,6 +21,10 @@ const styles = StyleSheet.create({
},
});
+afterEach(() => {
+ jest.restoreAllMocks();
+});
+
describe('Card', () => {
it('renders an outlined card', async () => {
const tree = (await render({null} )).toJSON();
@@ -32,40 +32,49 @@ describe('Card', () => {
expect(tree).toMatchSnapshot();
});
- it('renders an outlined card with custom border radius and color', async () => {
+ it('renders an outlined card with a custom outline color', async () => {
+ const testID = 'custom-outline-card';
+
await render(
{null}
);
- expect(screen.getByTestId('card-outline')).toHaveStyle({
- borderRadius: 32,
+ expect(screen.getByTestId(`${testID}-outline`)).toHaveStyle({
borderColor: 'purple',
+ borderWidth: 1,
});
});
it('renders an outlined card with custom border color', async () => {
+ const testID = 'custom-border-card';
+
await render(
{null}
);
- expect(screen.getByLabelText('card')).toHaveStyle({
+ expect(screen.getByTestId(`${testID}-outline`)).toHaveStyle({
borderColor: Palette.error50,
+ borderWidth: 1,
});
});
- it('renders with a custom theme', async () => {
+ it('renders with a custom theme background color', async () => {
+ jest.replaceProperty(Platform, 'OS', 'web');
+
await render(
{
screen.getByTestId('card-actions').props.children[0].props.mode
).toBe('contained');
});
-
- it('renders button with custom styles', async () => {
- await render(
-
-
-
- Agree
-
-
-
- );
-
- expect(screen.getByTestId('card-actions-button')).toHaveStyle({
- borderRadius: 32,
- });
- });
});
describe('getCardColors - background color', () => {
@@ -224,32 +213,3 @@ describe('getCardCoverStyle - border radius', () => {
).toMatchObject({ borderRadius: getTheme().shapes.corner.medium });
});
});
-
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- {null}
-
- );
- expect(screen.getByTestId('card-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('card-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
diff --git a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
index d1492cc475..5c39f6bf05 100644
--- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
+++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
@@ -4,27 +4,33 @@ exports[`Card renders an outlined card 1`] = `
-
-
-
+
-
+ "shadowOpacity": 0,
+ "shadowRadius": 0,
+ },
+ ]
+ }
+ />
+
+
`;
diff --git a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap
index 54f2e4f7a4..714b65414e 100644
--- a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap
+++ b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap
@@ -83,6 +83,7 @@ exports[`renders Checkbox with custom testID 1`] = `
}
>
{
});
});
-describe('getChipColor - selected background color', () => {
- it('should return custom color, outlined mode', () => {
- expect(
- getChipColors({
- theme: getTheme(),
- customBackgroundColor: 'purple',
- isOutlined: true,
- })
- ).toMatchObject({
- selectedBackgroundColor: 'purple',
- });
- });
-
- it('should return custom color, flat mode', () => {
- expect(
- getChipColors({
- theme: getTheme(),
- customBackgroundColor: 'purple',
- isOutlined: false,
- })
- ).toMatchObject({
- selectedBackgroundColor: 'purple',
- });
- });
-
- it('should return theme color, for theme version 3, flat mode', () => {
- expect(
- getChipColors({
- theme: getTheme(),
- isOutlined: false,
- })
- ).toMatchObject({
- selectedBackgroundColor: getTheme().colors.secondaryContainer,
- });
- });
-});
-
describe('getChipColor - background color', () => {
- it('should return custom color', () => {
- expect(
- getChipColors({
- theme: getTheme(),
- customBackgroundColor: 'purple',
- isOutlined: false,
- })
- ).toMatchObject({
- backgroundColor: 'purple',
- });
- });
-
it('should return theme color, for theme version 3, outlined mode', () => {
expect(
getChipColors({
@@ -317,18 +265,6 @@ describe('getChipColor - border color', () => {
});
});
- it('should return custom color, flat mode', () => {
- expect(
- getChipColors({
- theme: getTheme(true),
- customBackgroundColor: 'purple',
- isOutlined: false,
- })
- ).toMatchObject({
- borderColor: 'transparent',
- });
- });
-
it('should return theme color, light mode, outlined mode', () => {
expect(
getChipColors({
@@ -373,32 +309,3 @@ describe('getChipColor - border color', () => {
});
});
});
-
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
- {}}
- testID="chip"
- style={[{ transform: [{ scale: value }] }]}
- >
- Example Chip
-
- );
- expect(screen.getByTestId('chip-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('chip-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
diff --git a/src/components/__tests__/FABExtended.test.tsx b/src/components/__tests__/FABExtended.test.tsx
index fd9a21cec9..e8e530484d 100644
--- a/src/components/__tests__/FABExtended.test.tsx
+++ b/src/components/__tests__/FABExtended.test.tsx
@@ -1,9 +1,30 @@
-import { expect, it, jest } from '@jest/globals';
+import { Platform } from 'react-native';
+
+import { afterEach, expect, it, jest } from '@jest/globals';
import { fireEvent, userEvent } from '@testing-library/react-native';
+import * as Reanimated from 'react-native-reanimated';
import { render, screen } from '../../test-utils';
import FAB from '../FAB';
+jest.mock('react-native-reanimated', () => {
+ const ReanimatedModule = jest.requireActual<
+ typeof import('react-native-reanimated')
+ >('react-native-reanimated');
+
+ return {
+ __esModule: true,
+ ...ReanimatedModule,
+ default: ReanimatedModule.default,
+ measure: jest.fn(),
+ };
+});
+
+afterEach(() => {
+ jest.mocked(Reanimated.measure).mockReset();
+ jest.restoreAllMocks();
+});
+
it('renders extended FAB expanded', async () => {
const tree = (
await render( )
@@ -11,6 +32,33 @@ it('renders extended FAB expanded', async () => {
expect(tree).toMatchSnapshot();
});
+it('expands to fit the measured label width', async () => {
+ jest.replaceProperty(Platform, 'OS', 'web');
+ jest.mocked(Reanimated.measure).mockReturnValue({
+ x: 0,
+ y: 0,
+ width: 80,
+ height: 20,
+ pageX: 0,
+ pageY: 0,
+ });
+
+ await render(
+
+ );
+ await jest.runAllTimersAsync();
+
+ expect(
+ Reanimated.getAnimatedStyle(screen.getByTestId('extended-fab-container'))
+ ).toMatchObject({ width: 144 });
+});
+
it('renders extended FAB collapsed', async () => {
const tree = (
await render(
diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx
index b28456c5ce..a8bc540aa9 100644
--- a/src/components/__tests__/IconButton.test.tsx
+++ b/src/components/__tests__/IconButton.test.tsx
@@ -1,7 +1,6 @@
-import { Animated, StyleSheet } from 'react-native';
+import { StyleSheet } from 'react-native';
-import { describe, expect, it, jest } from '@jest/globals';
-import { act } from '@testing-library/react-native';
+import { describe, expect, it } from '@jest/globals';
import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
@@ -318,30 +317,3 @@ describe('getIconButtonColor - border color', () => {
});
});
});
-
-it('action animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- );
- expect(screen.getByTestId('icon-button-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('icon-button-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx
index bd14689f70..5fdc76f64d 100644
--- a/src/components/__tests__/Menu.test.tsx
+++ b/src/components/__tests__/Menu.test.tsx
@@ -1,4 +1,4 @@
-import { Animated, Dimensions, StyleSheet, View } from 'react-native';
+import { Dimensions, StyleSheet, View } from 'react-native';
import { expect, it, jest } from '@jest/globals';
import { act, screen, waitFor } from '@testing-library/react-native';
@@ -10,13 +10,6 @@ import Button from '../Button/Button';
import Menu from '../Menu/Menu';
import Portal from '../Portal/Portal';
-const styles = StyleSheet.create({
- contentStyle: {
- borderTopLeftRadius: 0,
- borderTopRightRadius: 0,
- },
-});
-
it('renders visible menu', async () => {
const tree = (
await render(
@@ -55,31 +48,12 @@ it('renders not visible menu', async () => {
expect(tree).toMatchSnapshot();
});
-it('renders menu with content styles', async () => {
- const tree = (
- await render(
-
- Open menu }
- contentStyle={styles.contentStyle}
- >
-
-
-
-
- )
- ).toJSON();
-
- expect(tree).toMatchSnapshot();
-});
-
const elevations: Elevation[] = [0, 1, 2, 3, 4, 5];
elevations.forEach((elevation) =>
it(`renders menu with background color based on elevation value = ${elevation}`, async () => {
const theme = getTheme();
+ const testID = 'menu-with-elevation';
await render(
@@ -88,6 +62,8 @@ elevations.forEach((elevation) =>
onDismiss={jest.fn()}
anchor={Open menu }
elevation={elevation}
+ mode="flat"
+ testID={testID}
>
@@ -95,13 +71,14 @@ elevations.forEach((elevation) =>
);
- expect(screen.getByTestId('menu-surface')).toHaveStyle({
+ expect(screen.getByTestId(`${testID}-surface`)).toHaveStyle({
backgroundColor: theme.colors.elevation[`level${elevation}`],
});
})
);
it('uses the default anchorPosition of top', async () => {
+ const testID = 'top-positioned-menu';
const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({
width: 400,
height: 800,
@@ -123,7 +100,7 @@ it('uses the default anchorPosition of top', async () => {
Open menu
}
- contentStyle={styles.contentStyle}
+ testID={testID}
>
@@ -145,7 +122,7 @@ it('uses the default anchorPosition of top', async () => {
});
await waitFor(() => {
- const menu = screen.getByTestId('menu-view');
+ const menu = screen.getByTestId(`${testID}-view`);
expect(menu).toHaveStyle({
position: 'absolute',
left: 100,
@@ -158,6 +135,7 @@ it('uses the default anchorPosition of top', async () => {
});
it('respects anchorPosition bottom', async () => {
+ const testID = 'bottom-positioned-menu';
const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({
width: 400,
height: 800,
@@ -180,7 +158,7 @@ it('respects anchorPosition bottom', async () => {
}
anchorPosition="bottom"
- contentStyle={styles.contentStyle}
+ testID={testID}
>
@@ -198,7 +176,7 @@ it('respects anchorPosition bottom', async () => {
});
await waitFor(() => {
- const menu = screen.getByTestId('menu-view');
+ const menu = screen.getByTestId(`${testID}-view`);
expect(menu).toHaveStyle({
position: 'absolute',
left: 100,
@@ -210,40 +188,9 @@ it('respects anchorPosition bottom', async () => {
dimensionsSpy.mockRestore();
});
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- Open menu}
- testID="menu"
- contentStyle={[{ transform: [{ scale: value }] }]}
- >
-
-
-
- );
- expect(screen.getByTestId('menu-surface-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('menu-surface-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
-
it('renders menu with mode "elevated"', async () => {
+ const testID = 'elevated-menu';
+
await render(
{
onDismiss={jest.fn()}
anchor={Open menu }
mode="elevated"
+ testID={testID}
>
@@ -258,7 +206,7 @@ it('renders menu with mode "elevated"', async () => {
);
- const menuSurface = screen.getByTestId('menu-surface');
+ const menuSurface = screen.getByTestId(`${testID}-surface`);
// Get flattened styles
// eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion.
@@ -269,6 +217,8 @@ it('renders menu with mode "elevated"', async () => {
});
it('renders menu with mode "flat"', async () => {
+ const testID = 'flat-menu';
+
await render(
{
onDismiss={jest.fn()}
anchor={Open menu }
mode="flat"
+ testID={testID}
>
@@ -283,7 +234,7 @@ it('renders menu with mode "flat"', async () => {
);
- const menuSurface = screen.getByTestId('menu-surface');
+ const menuSurface = screen.getByTestId(`${testID}-surface`);
// Get flattened styles
// eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion.
diff --git a/src/components/__tests__/Modal.test.tsx b/src/components/__tests__/Modal.test.tsx
index ff4cf2fd41..83d644e0e1 100644
--- a/src/components/__tests__/Modal.test.tsx
+++ b/src/components/__tests__/Modal.test.tsx
@@ -1,4 +1,4 @@
-import { Animated, BackHandler as RNBackHandler, Text } from 'react-native';
+import { BackHandler as RNBackHandler, Text } from 'react-native';
import type { BackHandlerStatic as RNBackHandlerStatic } from 'react-native';
import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals';
@@ -111,7 +111,7 @@ describe('Modal', () => {
expect(onDismiss).toHaveBeenCalled();
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -123,7 +123,7 @@ describe('Modal', () => {
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -138,7 +138,7 @@ describe('Modal', () => {
);
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -150,7 +150,7 @@ describe('Modal', () => {
);
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -158,7 +158,7 @@ describe('Modal', () => {
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -166,9 +166,7 @@ describe('Modal', () => {
jest.runAllTimers();
});
- expect(
- screen.queryByTestId('modal-surface-outer-layer')
- ).not.toBeOnTheScreen();
+ expect(screen.queryByTestId('modal-surface')).not.toBeOnTheScreen();
expect(screen.queryByTestId('modal-backdrop')).not.toBeOnTheScreen();
});
@@ -182,7 +180,7 @@ describe('Modal', () => {
);
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -190,7 +188,7 @@ describe('Modal', () => {
BackHandler.mockPressBack();
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -202,7 +200,7 @@ describe('Modal', () => {
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -225,13 +223,13 @@ describe('Modal', () => {
);
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
await userEvent.press(screen.getByTestId('modal-backdrop'));
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -243,7 +241,7 @@ describe('Modal', () => {
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
});
@@ -288,7 +286,7 @@ describe('Modal', () => {
);
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -296,7 +294,7 @@ describe('Modal', () => {
BackHandler.mockPressBack();
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -308,7 +306,7 @@ describe('Modal', () => {
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
});
@@ -364,7 +362,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: 0,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 0,
});
@@ -375,7 +373,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
});
@@ -392,7 +390,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -405,7 +403,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -452,7 +450,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -465,7 +463,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -490,7 +488,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -503,7 +501,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
@@ -526,7 +524,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: scrimAlpha,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 1,
});
});
@@ -551,7 +549,7 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-backdrop')).toHaveStyle({
opacity: 0,
});
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
opacity: 0,
});
@@ -577,34 +575,4 @@ describe('Modal', () => {
});
});
});
-
- it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- {null}
-
- );
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.runAllTimers();
- });
-
- expect(screen.getByTestId('modal-surface-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
- });
});
diff --git a/src/components/__tests__/Searchbar.test.tsx b/src/components/__tests__/Searchbar.test.tsx
index 0671263277..5fea2800ed 100644
--- a/src/components/__tests__/Searchbar.test.tsx
+++ b/src/components/__tests__/Searchbar.test.tsx
@@ -1,7 +1,5 @@
-import { Animated } from 'react-native';
-
import { expect, it, jest } from '@jest/globals';
-import { act, userEvent } from '@testing-library/react-native';
+import { userEvent } from '@testing-library/react-native';
import { render, screen } from '../../test-utils';
import * as Avatar from '../Avatar/Avatar';
@@ -72,33 +70,6 @@ it('renders clear icon wrapper, which is never target of touch events, if search
).toBe('none');
});
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- );
- expect(screen.getByTestId('search-bar-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('search-bar-container-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
-
it('defines onClearIconPress action and checks if it is called when close button is pressed', async () => {
const onClearIconPressMock = jest.fn();
await render(
diff --git a/src/components/__tests__/Snackbar.test.tsx b/src/components/__tests__/Snackbar.test.tsx
index 16b118da09..5c30f7cc6e 100644
--- a/src/components/__tests__/Snackbar.test.tsx
+++ b/src/components/__tests__/Snackbar.test.tsx
@@ -1,9 +1,8 @@
-import { Animated, StyleSheet, Text, View } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
import { expect, it, jest } from '@jest/globals';
-import { act } from '@testing-library/react-native';
-import { render, screen } from '../../test-utils';
+import { render } from '../../test-utils';
import { red200, white } from '../../theme/colors';
import Snackbar from '../Snackbar';
@@ -92,33 +91,3 @@ it('renders snackbar with View & Text as a child', async () => {
expect(tree).toMatchSnapshot();
});
-
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- Snackbar content
-
- );
- expect(screen.getByTestId('snack-bar-outer-layer')).toHaveStyle({
- transform: [{ scale: 1 }],
- });
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('snack-bar-outer-layer')).toHaveStyle({
- transform: [{ scale: 1.5 }],
- });
-});
diff --git a/src/components/__tests__/Surface.test.tsx b/src/components/__tests__/Surface.test.tsx
index d0a0437f65..889e51a25a 100644
--- a/src/components/__tests__/Surface.test.tsx
+++ b/src/components/__tests__/Surface.test.tsx
@@ -1,6 +1,9 @@
-import type { ViewStyle } from 'react-native';
-import { StyleSheet } from 'react-native';
-import { Platform } from 'react-native';
+import {
+ DynamicColorIOS,
+ Platform,
+ PlatformColor,
+ Pressable,
+} from 'react-native';
import {
afterEach,
@@ -10,34 +13,77 @@ import {
it,
jest,
} from '@jest/globals';
+import { userEvent } from '@testing-library/react-native';
+import {
+ getAnimatedStyle,
+ useAnimatedStyle,
+ useSharedValue,
+} from 'react-native-reanimated';
import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
import Surface from '../Surface';
-type StyleCase = {
- property: keyof ViewStyle;
- value: ViewStyle[keyof ViewStyle];
-};
-
const SPOT_SHADOW_OPACITY = 0.19;
const AMBIENT_SHADOW_OPACITY = 0.039;
+const AnimatedSurface = () => {
+ const opacity = useSharedValue(0);
+ const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
+
+ return (
+ <>
+ {
+ opacity.value = 1;
+ }}
+ />
+
+ {null}
+
+ >
+ );
+};
+
+const AnimatedVisualSurface = () => {
+ const borderRadius = useSharedValue(4);
+
+ return (
+ <>
+ {
+ borderRadius.value = 8;
+ }}
+ />
+
+ {null}
+
+ >
+ );
+};
+
afterEach(() => {
jest.restoreAllMocks();
});
describe('Surface', () => {
- it('should properly render passed props', async () => {
- await render(
-
- {null}
-
- );
- // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion.
- expect(screen.getByTestId('surface-container').props.pointerEvents).toBe(
- 'box-none'
- );
+ it('updates styles when a Reanimated shared value changes', async () => {
+ await render( );
+
+ const surface = screen.getByTestId('animated-surface');
+ expect(getAnimatedStyle(surface)).toMatchObject({ opacity: 0 });
+
+ await userEvent.press(screen.getByTestId('animate-surface'));
+ await jest.runAllTimersAsync();
+
+ expect(getAnimatedStyle(surface)).toMatchObject({ opacity: 1 });
});
describe('on iOS', () => {
@@ -45,42 +91,13 @@ describe('Surface', () => {
jest.replaceProperty(Platform, 'OS', 'ios');
});
- const styles = StyleSheet.create({
- absoluteStyles: {
- bottom: 10,
- end: 20,
- left: 30,
- position: 'absolute',
- right: 40,
- start: 50,
- top: 60,
- },
- innerLayerViewStyle: {
- padding: 13,
- },
- restStyle: {
- padding: 10,
- flexDirection: 'row',
- alignContent: 'center',
- },
- });
-
it('should render Surface with appropriate bg color but without shadow, if mode is set to "flat"', async () => {
await render(
-
+
{null}
);
- // @ts-expect-error
- expect(screen.getByTestId('surface-test-outer-layer')).not.toHaveStyle({
- shadowOpacity: expect.any(Number),
- });
// @ts-expect-error
expect(screen.getByTestId('surface-test')).not.toHaveStyle({
shadowOpacity: expect.any(Number),
@@ -90,192 +107,70 @@ describe('Surface', () => {
});
});
- it('should render a spot shadow over an ambient shadow, if mode is elevated', async () => {
+ it('should render a spot shadow if mode is elevated', async () => {
await render(
{null}
);
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('surface-test')).toHaveStyle({
shadowOpacity: SPOT_SHADOW_OPACITY,
});
+ });
+
+ it('applies background and shape props in flat mode', async () => {
+ const backgroundColor = 'rgba(1, 2, 3, 0.5)';
+ await render(
+
+ {null}
+
+ );
+
expect(screen.getByTestId('surface-test')).toHaveStyle({
- shadowOpacity: AMBIENT_SHADOW_OPACITY,
+ backgroundColor,
+ borderRadius: 4,
+ borderTopLeftRadius: 8,
});
});
- it.each([
- { property: 'opacity', value: 0.7 },
- { property: 'transform', value: [{ scale: 1.02 }] },
- { property: 'width', value: '42%' },
- { property: 'height', value: '32.5%' },
- { property: 'margin', value: 13 },
- { property: 'marginLeft', value: 13.1 },
- { property: 'marginRight', value: 13.2 },
- { property: 'marginTop', value: 13.3 },
- { property: 'marginBottom', value: 13.4 },
- { property: 'marginHorizontal', value: 13.5 },
- { property: 'marginVertical', value: 13.6 },
- { property: 'position', value: 'absolute' },
- { property: 'alignSelf', value: 'flex-start' },
- { property: 'top', value: 1.1 },
- { property: 'right', value: 1.2 },
- { property: 'bottom', value: 1.3 },
- { property: 'left', value: 1.4 },
- { property: 'start', value: 1.5 },
- { property: 'end', value: 1.6 },
- { property: 'flex', value: 6 },
- ] satisfies StyleCase[])(
- 'applies $property to outer layer only',
- async ({ property, value }) => {
- const style = { [property]: value };
-
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(
- style
- );
- expect(screen.getByTestId('surface-test')).not.toHaveStyle(style);
- }
- );
-
- it.each([
- { property: 'padding', value: 12 },
- { property: 'paddingLeft', value: 12.1 },
- { property: 'paddingRight', value: 12.2 },
- { property: 'paddingTop', value: 12.3 },
- { property: 'paddingBottom', value: 12.4 },
- { property: 'paddingHorizontal', value: 12.5 },
- { property: 'paddingVertical', value: 12.6 },
- { property: 'borderWidth', value: 2 },
- { property: 'borderColor', value: 'black' },
- ] satisfies StyleCase[])(
- 'applies $property to inner layer only',
- async ({ property, value }) => {
- const style = { [property]: value };
-
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test-outer-layer')).not.toHaveStyle(
- style
- );
- expect(screen.getByTestId('surface-test')).toHaveStyle(style);
- }
- );
-
- it.each([
- { property: 'borderRadius', value: 3 },
- { property: 'borderTopLeftRadius', value: 1 },
- { property: 'borderTopRightRadius', value: 2 },
- { property: 'borderBottomLeftRadius', value: 3 },
- { property: 'borderBottomRightRadius', value: 4 },
- { property: 'backgroundColor', value: 'rgb(4, 5, 6)' },
- ] satisfies StyleCase[])(
- 'applies $property to every layer',
- async ({ property, value }) => {
- const style = { [property]: value };
-
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(
- style
- );
- expect(screen.getByTestId('surface-test')).toHaveStyle(style);
- }
- );
-
- describe('outer layer', () => {
- it('should not render rest style', async () => {
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test-outer-layer')).not.toHaveStyle(
- styles.restStyle
- );
- });
+ it('updates the animated corner radius', async () => {
+ await render( );
- it('should render absolute position properties on outer layer', async () => {
- await render(
-
- {null}
-
- );
+ const surface = screen.getByTestId('animated-visual-surface');
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(
- styles.absoluteStyles
- );
+ expect(getAnimatedStyle(surface)).toMatchObject({
+ borderRadius: 4,
});
- it('should render absolute position properties on the outer layer', async () => {
- await render(
-
- {null}
-
- );
+ await userEvent.press(screen.getByTestId('animate-visual-props'));
+ await jest.runAllTimersAsync();
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(
- styles.absoluteStyles
- );
+ expect(getAnimatedStyle(surface)).toMatchObject({
+ borderRadius: 8,
});
});
- describe('inner layer', () => {
- it('should render inner layer styles on the inner layer', async () => {
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test')).toHaveStyle(
- styles.innerLayerViewStyle
- );
- });
- });
-
- it('applies backgroundColor to every layer', async () => {
- const backgroundColor = 'rgb(1, 2, 3)';
+ it('does not transition a DynamicColorIOS background', async () => {
await render(
{null}
);
- const style = { backgroundColor };
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(style);
- expect(screen.getByTestId('surface-test')).toHaveStyle(style);
- });
-
- describe('children wrapper', () => {
- it('should render rest styles', async () => {
- const combinedStyles = [styles.innerLayerViewStyle, styles.restStyle];
-
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test')).toHaveStyle(combinedStyles);
+ expect(
+ getAnimatedStyle(screen.getByTestId('surface-test'))
+ ).toMatchObject({
+ transitionProperty: expect.not.arrayContaining(['backgroundColor']),
});
});
});
@@ -287,12 +182,7 @@ describe('Surface', () => {
it('should render Surface with appropriate bg color but without shadow, if mode is set to "flat"', async () => {
await render(
-
+
{null}
);
@@ -317,6 +207,23 @@ describe('Surface', () => {
elevation: 12,
});
});
+
+ it('does not transition a PlatformColor background', async () => {
+ await render(
+
+ {null}
+
+ );
+
+ expect(
+ getAnimatedStyle(screen.getByTestId('surface-container'))
+ ).toMatchObject({
+ transitionProperty: expect.not.arrayContaining(['backgroundColor']),
+ });
+ });
});
describe('on Web', () => {
diff --git a/src/components/__tests__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx
index 1ea9e20bae..bd798e8180 100644
--- a/src/components/__tests__/ToggleButton.test.tsx
+++ b/src/components/__tests__/ToggleButton.test.tsx
@@ -1,10 +1,7 @@
-import { Animated } from 'react-native';
-
-import { describe, expect, it, jest } from '@jest/globals';
-import { act } from '@testing-library/react-native';
+import { describe, expect, it } from '@jest/globals';
import { getTheme } from '../../core/theming';
-import { render, screen } from '../../test-utils';
+import { render } from '../../test-utils';
import ToggleButton from '../ToggleButton';
import { getToggleButtonColor } from '../ToggleButton/utils';
@@ -55,36 +52,3 @@ describe('getToggleButtonColor', () => {
);
});
});
-
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
- await render(
-
- );
- expect(screen.getByTestId('toggle-button-container-outer-layer')).toHaveStyle(
- {
- transform: [{ scale: 1 }],
- }
- );
-
- Animated.timing(value, {
- toValue: 1.5,
- useNativeDriver: false,
- duration: 200,
- }).start();
-
- await act(() => {
- jest.advanceTimersByTime(200);
- });
- expect(screen.getByTestId('toggle-button-container-outer-layer')).toHaveStyle(
- {
- transform: [{ scale: 1.5 }],
- }
- );
-});
diff --git a/src/components/__tests__/__snapshots__/Badge.test.tsx.snap b/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
index 0090bbfbc0..3ffc31ac78 100644
--- a/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
@@ -3,6 +3,7 @@
exports[`renders badge 1`] = `
-
+
+
+
-
-
-
- Custom theme
-
-
-
-
+
+
+
-
+
+
-
+
+
-
-
- first
-
-
-
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ first
+
@@ -280,132 +398,193 @@ exports[`renders hidden banner, without action buttons and without image 1`] = `
-
+
+
-
-
+
+
-
-
- Two line text string with two actions. One to two lines is preferable on mobile.
-
-
-
+ >
+ Two line text string with two actions. One to two lines is preferable on mobile.
+
+
@@ -415,148 +594,242 @@ exports[`renders visible banner, with action buttons and with image 1`] = `
-
+
+
+
-
-
-
-
-
+
+
- Two line text string with two actions. One to two lines is preferable on mobile.
-
-
-
-
+
+
+
-
+
+
-
+
+
-
-
- first
-
-
-
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ first
+
@@ -713,126 +1010,220 @@ exports[`renders visible banner, with action buttons and without image 1`] = `
-
+
+
+
-
-
-
- Two line text string with two actions. One to two lines is preferable on mobile.
-
-
-
-
+
+
+
-
+
+
-
+
+
-
-
- first
-
-
-
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ first
+
-
+
-
+
+
-
+
+
-
-
- second
-
-
-
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ second
+
@@ -1141,142 +1614,202 @@ exports[`renders visible banner, without action buttons and with image 1`] = `
-
+
+
+
-
-
-
-
-
+
+
- Two line text string with two actions. One to two lines is preferable on mobile.
-
-
-
+ >
+ Two line text string with two actions. One to two lines is preferable on mobile.
+
+
@@ -1286,120 +1819,180 @@ exports[`renders visible banner, without action buttons and without image 1`] =
-
+
+
+
-
-
-
- Two line text string with two actions. One to two lines is preferable on mobile.
-
-
-
+ >
+ Two line text string with two actions. One to two lines is preferable on mobile.
+
+
diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
index 33855bb467..6cf569a0c5 100644
--- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
@@ -74,42 +74,102 @@ exports[`allows customizing Route's type via generics 1`] = `
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
-
-
- Button with accessibility hint
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Button with accessibility hint
+
@@ -155,34 +210,42 @@ exports[`renders button with an accessibility hint 1`] = `
exports[`renders button with an accessibility label 1`] = `
-
+
+
-
+
-
-
- Button with accessibility label
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Button with accessibility label
+
@@ -310,31 +420,39 @@ exports[`renders button with button color 1`] = `
-
+
+
-
+
-
-
- Custom Button
-
-
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Custom Button
+
+
+
`;
@@ -462,31 +627,39 @@ exports[`renders button with color 1`] = `
-
+
+
-
+
-
-
- Custom Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Custom Button
+
@@ -614,31 +834,39 @@ exports[`renders button with custom testID 1`] = `
-
+
+
-
+
-
-
- Button with custom testID
-
-
+ ],
+ ]
+ }
+ testID="custom:testID-text"
+ >
+ Button with custom testID
+
@@ -766,31 +1041,39 @@ exports[`renders button with icon 1`] = `
-
+
+
-
+
-
-
- camera
-
-
-
+ camera
+
+
+
- Icon Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Icon Button
+
@@ -967,31 +1297,39 @@ exports[`renders button with icon in reverse order 1`] = `
-
+
+
-
+
-
-
- chevron-right
-
-
-
+ chevron-right
+
+
+
- Right Icon
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Right Icon
+
@@ -1170,31 +1555,39 @@ exports[`renders contained contained with mode 1`] = `
-
+
+
-
+
-
-
- Contained Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Contained Button
+
@@ -1323,31 +1763,39 @@ exports[`renders disabled button 1`] = `
-
+
+
-
+
-
-
- Disabled Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Disabled Button
+
@@ -1475,31 +1970,39 @@ exports[`renders loading button 1`] = `
-
+
+
-
+
@@ -1610,13 +2175,13 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "alignItems": "center",
- "bottom": 0,
- "justifyContent": "center",
- "left": 0,
- "position": "absolute",
- "right": 0,
- "top": 0,
+ "height": 18,
+ "transform": [
+ {
+ "rotate": "45deg",
+ },
+ ],
+ "width": 18,
}
}
>
@@ -1624,12 +2189,8 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "rotate": "45deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
"width": 18,
}
}
@@ -1638,8 +2199,15 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
- "overflow": "hidden",
+ "height": 18,
+ "transform": [
+ {
+ "translateY": 0,
+ },
+ {
+ "rotate": "-165deg",
+ },
+ ],
"width": 18,
}
}
@@ -1648,15 +2216,8 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "translateY": 0,
- },
- {
- "rotate": "-165deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
"width": 18,
}
}
@@ -1665,40 +2226,44 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
- "overflow": "hidden",
+ "borderColor": "rgba(103, 80, 164, 1)",
+ "borderRadius": 9,
+ "borderWidth": 1.8,
+ "height": 18,
"width": 18,
}
}
- >
-
-
+ />
+
+
@@ -1706,12 +2271,9 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "rotate": "45deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
+ "top": 9,
"width": 18,
}
}
@@ -1720,9 +2282,15 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
- "overflow": "hidden",
- "top": 9,
+ "height": 18,
+ "transform": [
+ {
+ "translateY": -9,
+ },
+ {
+ "rotate": "345deg",
+ },
+ ],
"width": 18,
}
}
@@ -1731,15 +2299,8 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "translateY": -9,
- },
- {
- "rotate": "345deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
"width": 18,
}
}
@@ -1748,114 +2309,111 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
- "overflow": "hidden",
+ "borderColor": "rgba(103, 80, 164, 1)",
+ "borderRadius": 9,
+ "borderWidth": 1.8,
+ "height": 18,
"width": 18,
}
}
- >
-
-
+ />
-
+
- Loading Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Loading Button
+
`;
-exports[`renders outlined button with mode 1`] = `
-
-
+
+
-
+
-
-
- Outlined Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Outlined Button
+
@@ -1984,31 +2589,39 @@ exports[`renders text button by default 1`] = `
-
+
+
-
+
-
-
- Text Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Text Button
+
@@ -2136,31 +2796,39 @@ exports[`renders text button with mode 1`] = `
-
+
+
-
+
-
-
- Text Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Text Button
+
diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
index 7bf18dde0e..04359a41f9 100644
--- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
@@ -4,31 +4,41 @@ exports[`renders chip with close button 1`] = `
-
+
+
-
+
-
-
- information
-
-
- Example Chip
+ information
-
-
-
-
-
- close
-
-
-
+ {
+ "marginLeft": 8,
+ "marginRight": 0,
+ },
+ undefined,
+ ],
+ ],
+ ]
+ }
+ >
+ Example Chip
+
-
-`;
-
-exports[`renders chip with custom close button 1`] = `
-
-
-
- information
-
-
- Example Chip
+ close
-
-
-
-
- arrow-down
-
-
-
-
`;
-exports[`renders chip with icon 1`] = `
+exports[`renders chip with custom close button 1`] = `
-
+
+
-
+
-
-
- information
-
-
-
+ information
+
+
+
- Example Chip
-
-
+ ],
+ ]
+ }
+ >
+ Example Chip
+
-
-`;
-
-exports[`renders chip with onPress 1`] = `
-
- Example Chip
+ arrow-down
@@ -961,35 +710,45 @@ exports[`renders chip with onPress 1`] = `
`;
-exports[`renders outlined disabled chip 1`] = `
+exports[`renders chip with icon 1`] = `
-
+
+
-
+
+ information
+
+
+
- Example Chip
-
-
+ ],
+ ]
+ }
+ >
+ Example Chip
+
`;
-exports[`renders selected chip 1`] = `
+exports[`renders chip with onPress 1`] = `
+
+
+
+ Example Chip
+
+
+
+
+`;
+
+exports[`renders outlined disabled chip 1`] = `
+
+
+
-
+
+
+ Example Chip
+
+
+
+
+`;
+
+exports[`renders selected chip 1`] = `
+
+
+
+
-
-
- check
-
-
-
+ check
+
+
+
- Example Chip
-
-
+ ],
+ ]
+ }
+ >
+ Example Chip
+
diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
index e9bc774f78..349fa1d224 100644
--- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
@@ -382,280 +382,240 @@ exports[`DataTable.Pagination renders data table pagination 1`] = `
-
+
-
+
-
-
- chevron-left
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-left
+
-
+
-
+
-
-
- chevron-right
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-right
+
@@ -720,560 +680,480 @@ exports[`DataTable.Pagination renders data table pagination with fast-forward bu
+
-
-
- page-first
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ page-first
+
-
+
-
+
-
-
- chevron-left
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-left
+
-
+
-
+
-
-
- chevron-right
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-right
+
-
+
-
+
-
-
- page-last
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ page-last
+
@@ -1338,280 +1218,240 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = `
-
+
-
+
-
-
- chevron-left
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-left
+
-
+
-
+
-
-
- chevron-right
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-right
+
@@ -1684,200 +1524,256 @@ exports[`DataTable.Pagination renders data table pagination with options select
-
+
+
-
-
- menu-down
-
-
-
+ menu-down
+
+
+
- 2
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ 2
+
@@ -1924,560 +1820,480 @@ exports[`DataTable.Pagination renders data table pagination with options select
-
+
-
+
-
-
- page-first
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ page-first
+
-
+
-
+
-
-
- chevron-left
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-left
+
-
+
-
+
-
-
- chevron-right
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-right
+
-
+
-
+
-
-
- page-last
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ page-last
+
diff --git a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap
index cec33bb556..391413183a 100644
--- a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap
@@ -2,15 +2,15 @@
exports[`renders FAB large size 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
- camera
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ camera
+
@@ -146,139 +126,119 @@ exports[`renders icon button by default 1`] = `
-
+
-
+
-
-
- camera
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ camera
+
@@ -288,139 +248,119 @@ exports[`renders icon button with color 1`] = `
-
+
-
+
-
-
- camera
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ camera
+
@@ -430,139 +370,119 @@ exports[`renders icon button with size 1`] = `
-
+
-
+
-
-
- camera
-
-
+ ],
+ ]
+ }
+ >
+ camera
+
@@ -572,172 +492,152 @@ exports[`renders icon change animated 1`] = `
-
+
-
+
+
-
-
-
- camera
-
-
+ camera
+
diff --git a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
index f06b87045f..032971c85e 100644
--- a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
@@ -120,31 +120,41 @@ exports[`renders list item with custom description 1`] = `
-
+
+
-
+
-
-
- file-pdf-box
-
-
-
+ file-pdf-box
+
+
+
- DOCS.pdf
-
-
+ ],
+ ]
+ }
+ >
+ DOCS.pdf
+
diff --git a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap
index 3e027106ae..afcd649f4f 100644
--- a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap
@@ -1,25 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renders menu with content styles 1`] = `
-<>
+exports[`renders not visible menu 1`] = `
+
-
-
+
-
-
-
- Open menu
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
- Undo
-
-
-
-
-
-
-
-
- Redo
-
-
-
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Open menu
+
-
->
-`;
-
-exports[`renders not visible menu 1`] = `
-
+
+
+`;
+
+exports[`renders visible menu 1`] = `
+<>
+
-
-`;
-
-exports[`renders visible menu 1`] = `
-<>
-
-
-
-
-
-
-
- Open menu
-
-
-
-
-
-
-
+
+ />
-
-
+
+
+
-
-
-
- magnify
-
-
-
+ magnify
+
-
+
+
+
@@ -245,13 +292,13 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "alignItems": "center",
- "bottom": 0,
- "justifyContent": "center",
- "left": 0,
- "position": "absolute",
- "right": 0,
- "top": 0,
+ "height": 24,
+ "transform": [
+ {
+ "rotate": "45deg",
+ },
+ ],
+ "width": 24,
}
}
>
@@ -259,12 +306,8 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 24,
- "transform": [
- {
- "rotate": "45deg",
- },
- ],
+ "height": 12,
+ "overflow": "hidden",
"width": 24,
}
}
@@ -273,8 +316,15 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 12,
- "overflow": "hidden",
+ "height": 24,
+ "transform": [
+ {
+ "translateY": 0,
+ },
+ {
+ "rotate": "-165deg",
+ },
+ ],
"width": 24,
}
}
@@ -283,15 +333,8 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 24,
- "transform": [
- {
- "translateY": 0,
- },
- {
- "rotate": "-165deg",
- },
- ],
+ "height": 12,
+ "overflow": "hidden",
"width": 24,
}
}
@@ -300,40 +343,44 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 12,
- "overflow": "hidden",
+ "borderColor": "rgba(103, 80, 164, 1)",
+ "borderRadius": 12,
+ "borderWidth": 2.4,
+ "height": 24,
"width": 24,
}
}
- >
-
-
+ />
+
+
@@ -341,12 +388,9 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 24,
- "transform": [
- {
- "rotate": "45deg",
- },
- ],
+ "height": 12,
+ "overflow": "hidden",
+ "top": 12,
"width": 24,
}
}
@@ -355,9 +399,15 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 12,
- "overflow": "hidden",
- "top": 12,
+ "height": 24,
+ "transform": [
+ {
+ "translateY": -12,
+ },
+ {
+ "rotate": "345deg",
+ },
+ ],
"width": 24,
}
}
@@ -366,15 +416,8 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 24,
- "transform": [
- {
- "translateY": -12,
- },
- {
- "rotate": "345deg",
- },
- ],
+ "height": 12,
+ "overflow": "hidden",
"width": 24,
}
}
@@ -383,25 +426,14 @@ exports[`activity indicator snapshot test 1`] = `
collapsable={false}
style={
{
- "height": 12,
- "overflow": "hidden",
+ "borderColor": "rgba(103, 80, 164, 1)",
+ "borderRadius": 12,
+ "borderWidth": 2.4,
+ "height": 24,
"width": 24,
}
}
- >
-
-
+ />
@@ -416,29 +448,34 @@ exports[`renders with placeholder 1`] = `
-
-
+
+
+
-
-
-
- magnify
-
-
-
+ magnify
+
-
+
+
+
-
-
-
-
- close
-
-
-
+ close
+
@@ -789,29 +833,34 @@ exports[`renders with text 1`] = `
-
-
+
+
+
-
-
-
- magnify
-
-
-
+ magnify
+
-
+
+
+
-
-
-
-
- close
-
-
-
+ close
+
diff --git a/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap
index 76d4857c89..095b214bb2 100644
--- a/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap
@@ -21,69 +21,122 @@ exports[`renders snackbar with Text as a child 1`] = `
}
>
-
+
+
-
-
-
- Snackbar content
-
-
+
+
+ Snackbar content
+
@@ -109,94 +162,147 @@ exports[`renders snackbar with View & Text as a child 1`] = `
}
>
-
+
+
-
+
-
+ "alignItems": "center",
+ "flexDirection": "row",
+ }
+ }
+ >
-
-
+
- Error Message which is veryyyyyyyyyyyy longggggggg Error Message which is veryyyyyyyyyyyy longggggggg
-
-
+ }
+ >
+ Error Message which is veryyyyyyyyyyyy longggggggg Error Message which is veryyyyyyyyyyyy longggggggg
+
@@ -223,109 +329,195 @@ exports[`renders snackbar with action button 1`] = `
}
>
-
+
-
+
- Snackbar content
-
+ ],
+ ]
+ }
+ >
+ Snackbar content
+
+
-
-
+
+
-
+
+
-
-
- Undo
-
-
-
+ false,
+ false,
+ {
+ "color": "rgba(208, 188, 255, 1)",
+ "fontFamily": "System",
+ "fontSize": 14,
+ "fontWeight": "500",
+ "letterSpacing": 0.1,
+ "lineHeight": 20,
+ },
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Undo
+
@@ -494,87 +710,140 @@ exports[`renders snackbar with content 1`] = `
}
>
-
+
-
+
- Snackbar content
-
-
+ ],
+ ]
+ }
+ >
+ Snackbar content
+
`;
diff --git a/src/components/__tests__/__snapshots__/Switch.test.tsx.snap b/src/components/__tests__/__snapshots__/Switch.test.tsx.snap
index 3589a26ee9..e38a5145e1 100644
--- a/src/components/__tests__/__snapshots__/Switch.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Switch.test.tsx.snap
@@ -96,6 +96,7 @@ exports[`Switch render renders disabled off 1`] = `
-
+
-
+
-
-
- magnify
-
-
+ ],
+ ]
+ }
+ >
+ magnify
+
-
+
-
+
-
-
- close
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ close
+
@@ -572,6 +548,7 @@ exports[`renders filled TextInput with TextInput.Icon accessories when error is
}
/>
-
+
-
+
-
-
- magnify
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ magnify
+
-
+
-
+
-
-
- close
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ close
+
@@ -1074,6 +1026,7 @@ exports[`renders filled TextInput with label and value 1`] = `
}
/>
-
+
-
+
-
-
- magnify
-
-
+ ],
+ ]
+ }
+ >
+ magnify
+
-
+
-
+
-
-
- close
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ close
+
@@ -1753,6 +1682,7 @@ exports[`renders outlined TextInput with TextInput.Icon accessories when error i
/>
-
+
-
+
-
-
- magnify
-
-
+ ],
+ ]
+ }
+ >
+ magnify
+
-
+
-
+
-
-
- close
-
-
+ ],
+ ]
+ }
+ >
+ close
+
@@ -2236,6 +2140,7 @@ exports[`renders outlined TextInput with label and value 1`] = `
/>
+
-
-
- heart
-
-
+ ],
+ ]
+ }
+ >
+ heart
+
@@ -144,137 +134,127 @@ exports[`renders toggle button 1`] = `
+
-
-
- heart
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ heart
+
@@ -284,137 +264,127 @@ exports[`renders unchecked toggle button 1`] = `
+
-
-
- heart
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ heart
+
diff --git a/src/theme/tokens/sys/elevation.ts b/src/theme/tokens/sys/elevation.ts
index 18e92dcb63..9a1181ecbe 100644
--- a/src/theme/tokens/sys/elevation.ts
+++ b/src/theme/tokens/sys/elevation.ts
@@ -1,34 +1,11 @@
// M3 elevation tokens and shadow builder per spec:
// https://m3.material.io/styles/elevation/tokens
-import {
- Animated,
- Platform,
- type ColorValue,
- type ViewStyle,
- type Animated as AnimatedNS,
-} from 'react-native';
+import { Platform, type ColorValue, type ViewStyle } from 'react-native';
import color from 'color';
-import { isAnimatedValue } from '../../../utils/animations';
-import type { Elevation, ThemeElevation } from '../../types';
-
-type AnimatedNativeShadowStyle = {
- shadowColor: ColorValue;
- shadowOffset: {
- width: AnimatedNS.Value;
- height: AnimatedNS.AnimatedInterpolation;
- };
- shadowOpacity: AnimatedNS.AnimatedInterpolation;
- shadowRadius: AnimatedNS.AnimatedInterpolation;
-};
-
-type AnimatedBoxShadowStyle = {
- boxShadow: AnimatedNS.AnimatedInterpolation;
-};
-
-type AnimatedShadowStyle = AnimatedNativeShadowStyle | AnimatedBoxShadowStyle;
+import type { ThemeElevation } from '../../types';
export const defaultElevation: ThemeElevation = {
level0: 0,
@@ -39,8 +16,6 @@ export const defaultElevation: ThemeElevation = {
level5: 5,
};
-export const elevationInputRange: Elevation[] = Object.values(defaultElevation);
-
export const androidElevationLevels = [0, 1, 3, 6, 8, 12];
/**
@@ -110,7 +85,7 @@ const IOS_SHADOW_RADIUS_FACTOR = 0.5;
const getShadowRadius = (blurRadius: number[]) =>
blurRadius.map((radius) => round(radius * IOS_SHADOW_RADIUS_FACTOR));
-export const shadowLayers = [
+const shadowLayers = [
{
height: androidElevationLevels.map((dp) =>
round(
@@ -130,94 +105,58 @@ export const shadowLayers = [
},
];
-const getShadowColor = (shadowColor: ColorValue, shadowOpacity: number) => {
- if (typeof shadowColor !== 'string') {
- throw new Error(
- `Expected a string shadow color on Web, but received a ${typeof shadowColor}.`
- );
- }
-
- return color(shadowColor).alpha(shadowOpacity).rgb().string();
+type NativeShadowStyle = {
+ shadowColor: ColorValue;
+ shadowOpacity: number;
+ shadowOffset: {
+ width: number;
+ height: number;
+ };
+ shadowRadius: number;
};
-const getBoxShadowValue = (elevation: number, layerColors: readonly string[]) =>
- shadowLayers
- .map(
- (layer, index) =>
- `0px ${layer.height[elevation]}px ${layer.blurRadius[elevation]}px ${layerColors[index]}`
- )
- .join(', ');
+type ShadowStyle =
+ | NativeShadowStyle
+ | { boxShadow: NonNullable };
-export function shadow(elevation: number, shadowColor: ColorValue): ViewStyle;
-// eslint-disable-next-line no-redeclare
export function shadow(
- elevation: Animated.Value,
+ elevation: number,
shadowColor: ColorValue
-): AnimatedShadowStyle;
-// eslint-disable-next-line no-redeclare
-export function shadow(
- elevation: number | Animated.Value,
- shadowColor: ColorValue
-): ViewStyle | AnimatedShadowStyle;
-// eslint-disable-next-line no-redeclare
-export function shadow(
- elevation: number | Animated.Value = 0,
- shadowColor: ColorValue
-): ViewStyle | AnimatedShadowStyle {
+): [ShadowStyle, NativeShadowStyle | undefined] {
if (Platform.OS === 'web') {
- const layerColors = shadowLayers.map((layer) =>
- getShadowColor(shadowColor, layer.shadowOpacity)
- );
-
- if (isAnimatedValue(elevation)) {
- return {
- boxShadow: elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: elevationInputRange.map((value) =>
- getBoxShadowValue(value, layerColors)
- ),
- }),
- };
+ if (typeof shadowColor !== 'string') {
+ throw new Error(
+ `Expected a string shadow color on Web, but received a ${typeof shadowColor}.`
+ );
}
- return {
- boxShadow: getBoxShadowValue(elevation, layerColors),
- };
- }
-
- // For a single view, we can only draw one shadow
- // So we pick the spot shadow, as it shows the depth
- const [spotShadow] = shadowLayers;
-
- if (isAnimatedValue(elevation)) {
- return {
- shadowColor,
- shadowOffset: {
- width: new Animated.Value(0),
- height: elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: spotShadow.height,
- }),
+ return [
+ {
+ boxShadow: shadowLayers
+ .map(
+ (layer) =>
+ `0px ${layer.height[elevation]}px ${layer.blurRadius[elevation]}px ${color(
+ shadowColor
+ )
+ .alpha(layer.shadowOpacity)
+ .rgb()
+ .string()}`
+ )
+ .join(', '),
},
- shadowOpacity: elevation.interpolate({
- inputRange: [0, 1],
- outputRange: [0, spotShadow.shadowOpacity],
- extrapolate: 'clamp',
- }),
- shadowRadius: elevation.interpolate({
- inputRange: elevationInputRange,
- outputRange: spotShadow.shadowRadius,
- }),
- };
+ undefined,
+ ];
}
- return {
+ const [spotShadow, ambientShadow] = shadowLayers.map((layer) => ({
shadowColor,
- shadowOpacity: elevation ? spotShadow.shadowOpacity : 0,
+ shadowOpacity: elevation ? layer.shadowOpacity : 0,
shadowOffset: {
width: 0,
- height: spotShadow.height[elevation],
+ height: layer.height[elevation],
},
- shadowRadius: spotShadow.shadowRadius[elevation],
- };
+ shadowRadius: layer.shadowRadius[elevation],
+ }));
+
+ return [spotShadow, ambientShadow];
}
diff --git a/src/utils/animations.ts b/src/utils/animations.ts
deleted file mode 100644
index 7af142bbf3..0000000000
--- a/src/utils/animations.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Animated } from 'react-native';
-
-export const isAnimatedValue = (
- it: number | string | Animated.AnimatedInterpolation
-): it is Animated.Value => it instanceof Animated.Value;