+
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,123 @@ const styles = StyleSheet.create({
-### children (required)
+### backgroundColor
-
+
+
+
+
+### borderRadius
+
+
+
+
+
+
+
+### borderBottomEndRadius
+
+
+
+
+
+
+
+### borderBottomLeftRadius
+
+
+
+
+
+
+
+### borderBottomRightRadius
+
+
+
+
+
+
+
+### borderBottomStartRadius
+
+
+
+
+
+
+
+### borderEndEndRadius
+
+
+
+
+
+
+
+### borderEndStartRadius
+
+
+
+
+
+
+
+### borderStartEndRadius
+
+
+
+
+
+
+
+### borderStartStartRadius
+
+
+
+
+
+
+
+### borderTopEndRadius
+
+
+
+
+
+
+
+### borderTopLeftRadius
+
+
+
+
+
+
+
+### borderTopRightRadius
+
+
+
+
+
+
+
+### borderTopStartRadius
+
+
+
+
+
+
+
+### borderCurve
+
+
+
+
@@ -92,6 +204,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..d2661c8dec 100644
--- a/docs/6.x/docs/guides/migration.md
+++ b/docs/6.x/docs/guides/migration.md
@@ -4,8 +4,158 @@ 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 components no longer support overriding their background color or border radius with the `style` prop:
+
+- `Banner`
+- `Button`
+- `Card`
+- `Chip`
+- `Dialog`
+- `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.
+
+### Surface
+
+- The `elevation` prop no longer accepts a React Native `Animated.Value`. Any `elevation` changes are animated automatically.
+- The `style` prop no longer configures background and border radius. We have added new props for these:
+ - `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 border radius. 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..b322a31882 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,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp
"
+ "name": "ViewStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
}
@@ -304,19 +302,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 +414,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 +692,13 @@
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
- "elements": [
- {
- "name": "ViewStyle"
- }
- ],
- "raw": "StyleProp"
+ "name": "ViewStyle"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp"
},
"description": ""
},
@@ -1032,10 +1024,16 @@
"name": "StyleProp",
"elements": [
{
- "name": "TextStyle"
+ "name": "AnimatedStyle",
+ "elements": [
+ {
+ "name": "TextStyle"
+ }
+ ],
+ "raw": "AnimatedStyle"
}
],
- "raw": "StyleProp"
+ "raw": "StyleProp>"
},
"description": ""
},
@@ -1148,7 +1146,7 @@
"required": false,
"tsType": {
"name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
+ "raw": "0 | 1 | 2 | 3 | 4 | 5",
"elements": [
{
"name": "literal",
@@ -1173,9 +1171,6 @@
{
"name": "literal",
"value": "5"
- },
- {
- "name": "Animated.Value"
}
]
},
@@ -1195,19 +1190,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": ""
},
@@ -1234,7 +1229,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 +1268,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 +3277,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": ""
},
@@ -3450,7 +3501,7 @@
"required": false,
"tsType": {
"name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
+ "raw": "0 | 1 | 2 | 3 | 4 | 5",
"elements": [
{
"name": "literal",
@@ -3475,9 +3526,6 @@
{
"name": "literal",
"value": "5"
- },
- {
- "name": "Animated.Value"
}
]
},
@@ -3503,19 +3551,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": ""
},
@@ -3543,6 +3591,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 +4651,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": ""
},
@@ -4646,6 +4707,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 +5347,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": ""
},
@@ -6124,10 +6198,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 +6419,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 +8015,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 +9145,19 @@
"contentStyle": {
"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": "Style of menu's inner content."
},
@@ -9375,10 +9461,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 +9543,68 @@
"contentContainerStyle": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
- "name": "ViewStyle"
+ "name": "Omit",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ },
+ {
+ "name": "union",
+ "raw": "'backgroundColor' | 'borderRadius'",
+ "elements": [
+ {
+ "name": "literal",
+ "value": "'backgroundColor'"
+ },
+ {
+ "name": "literal",
+ "value": "'borderRadius'"
+ }
+ ]
+ }
+ ],
+ "raw": "Omit"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle>"
}
],
- "raw": "Animated.WithAnimatedValue>"
+ "raw": "StyleProp<\n AnimatedStyle>\n>"
+ },
+ "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": "Style for the content of the modal"
+ "description": "Elevation level of the modal content. Defaults to level 1."
},
"style": {
"required": false,
@@ -10547,7 +10680,7 @@
"required": false,
"tsType": {
"name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
+ "raw": "0 | 1 | 2 | 3 | 4 | 5",
"elements": [
{
"name": "literal",
@@ -10572,9 +10705,6 @@
{
"name": "literal",
"value": "5"
- },
- {
- "name": "Animated.Value"
}
]
},
@@ -10600,19 +10730,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": ""
},
@@ -11019,7 +11149,7 @@
"required": false,
"tsType": {
"name": "union",
- "raw": "0 | 1 | 2 | 3 | 4 | 5 | Animated.Value",
+ "raw": "0 | 1 | 2 | 3 | 4 | 5",
"elements": [
{
"name": "literal",
@@ -11044,9 +11174,6 @@
{
"name": "literal",
"value": "5"
- },
- {
- "name": "Animated.Value"
}
]
},
@@ -11092,19 +11219,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": ""
},
@@ -11145,54 +11272,166 @@
"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": "Extract['backgroundColor']",
+ "raw": "Extract<\n AnimatedStyle>>,\n Record\n>[Key]"
},
- "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."
},
"style": {
"required": false,
"tsType": {
- "name": "Animated.WithAnimatedValue",
+ "name": "StyleProp",
"elements": [
{
- "name": "StyleProp",
+ "name": "AnimatedStyle",
"elements": [
{
- "name": "ViewStyle"
+ "name": "Omit",
+ "elements": [
+ {
+ "name": "ViewStyle"
+ },
+ {
+ "name": "unknown"
+ }
+ ],
+ "raw": "Omit"
}
],
- "raw": "StyleProp"
+ "raw": "AnimatedStyle>"
}
],
- "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 +11468,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 +11495,6 @@
]
},
"description": ""
- },
- "container": {
- "required": false,
- "tsType": {
- "name": "boolean"
- },
- "description": "@internal"
}
}
},
@@ -11780,19 +12016,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/SurfaceExample.tsx b/example/src/Examples/SurfaceExample.tsx
index fe420830ce..c8225e5a5e 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,9 @@ const styles = StyleSheet.create({
},
verticalSurface: {
height: '48%',
+ },
+ verticalSurfaceContent: {
+ flex: 1,
justifyContent: 'center',
},
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/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx
index 8ef9965819..69308ebc8a 100644
--- a/src/components/Appbar/Appbar.tsx
+++ b/src/components/Appbar/Appbar.tsx
@@ -1,19 +1,22 @@
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 Props = Omit, 'style'> & {
/**
* Whether the background color is a dark color. A dark appbar will render light text and vice-versa.
@@ -51,7 +54,7 @@ export type Props = Omit, 'style'> & {
* @optional
*/
theme?: ThemeProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
};
/**
@@ -145,30 +148,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 borderRadius = getAppbarBorders(restStyle);
+
const isMode = (modeToCompare: AppbarModes) => {
return mode === modeToCompare;
};
@@ -210,23 +209,27 @@ const Appbar = ({
const insets = {
paddingBottom: safeAreaInsets?.bottom,
paddingTop: safeAreaInsets?.top,
- paddingLeft: safeAreaInsets?.left,
- paddingRight: safeAreaInsets?.right,
+ paddingLeft: safeAreaInsets?.left
+ ? safeAreaInsets.left + APPBAR_HORIZONTAL_PADDING
+ : APPBAR_HORIZONTAL_PADDING,
+ paddingRight: safeAreaInsets?.right
+ ? safeAreaInsets.right + APPBAR_HORIZONTAL_PADDING
+ : APPBAR_HORIZONTAL_PADDING,
};
return (
{shouldAddLeftSpacing ? : null}
@@ -308,7 +311,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..f9015d84c8 100644
--- a/src/components/Appbar/AppbarHeader.tsx
+++ b/src/components/Appbar/AppbarHeader.tsx
@@ -1,22 +1,17 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, View } from 'react-native';
+import { Platform, StyleSheet } from 'react-native';
import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Appbar } from './Appbar';
-import {
- getAppbarBackgroundColor,
- modeAppbarHeight,
- getAppbarBorders,
-} from './utils';
+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 +47,7 @@ export type Props = Omit<
* @optional
*/
theme?: ThemeProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp;
};
/**
@@ -100,64 +95,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;
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);
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..151485a589 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';
@@ -23,9 +23,8 @@ const borderStyleProperties = [
export const getAppbarBackgroundColor = (
theme: InternalTheme,
- _elevation: number,
- customBackground?: ColorValue,
- elevated?: boolean
+ elevated: boolean,
+ customBackground?: ColorValue
) => {
const { colors } = theme;
if (customBackground) {
@@ -54,19 +53,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..13599d3130 100644
--- a/src/components/Banner.tsx
+++ b/src/components/Banner.tsx
@@ -1,8 +1,22 @@
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,
+ type AnimatedStyle,
+} from 'react-native-reanimated';
+import { scheduleOnRN } from 'react-native-worklets';
import useLatestCallback from 'use-latest-callback';
import Button from './Button/Button';
@@ -11,11 +25,13 @@ import type { IconSource } from './Icon';
import Surface from './Surface';
import Text from './Typography/Text';
import { useInternalTheme } from '../core/theming';
-import type { $Omit, $RemoveChildren, ThemeProp } from '../types';
+import type { $RemoveChildren, 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?: 0 | 1 | 2 | 3 | 4 | 5;
/**
* 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,10 +461,15 @@ const BottomNavigationBar = ({
bottom: safeAreaInsets?.bottom ?? bottom,
};
+ const pointerEvents = layout.measured
+ ? keyboardHidesNavigationBar && keyboardVisible
+ ? 'none'
+ : 'auto'
+ : 'none';
+
return (
- ({
: null,
style,
]}
- pointerEvents={
- layout.measured
- ? keyboardHidesNavigationBar && keyboardVisible
- ? 'none'
- : 'auto'
- : 'none'
- }
- onLayout={onLayout}
- container
+ pointerEvents={pointerEvents}
>
-
-
- {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..a3c377b248 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,13 +7,16 @@ import type {
Role,
StyleProp,
TextStyle,
+ ViewProps,
ViewStyle,
} from 'react-native';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
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';
@@ -24,7 +27,7 @@ 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 +125,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 +195,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,43 +216,27 @@ 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();
+ 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();
+ setPressed(false);
}
};
@@ -321,9 +308,18 @@ const Button = ({
{...rest}
ref={ref}
testID={`${testID}-container`}
- style={[styles.button, compact && styles.compact, buttonStyle, style]}
+ backgroundColor={buttonStyle.backgroundColor}
+ {...touchableStyle}
+ style={[
+ styles.button,
+ compact && styles.compact,
+ {
+ borderColor,
+ borderWidth,
+ },
+ style,
+ ]}
elevation={elevation}
- container
>
{backgroundOpacity < 1 && (
, 'mode'> & {
+export type Props = Omit & {
/**
* Mode of the Card.
* - `elevated` - Card with elevation.
@@ -73,12 +77,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?: 0 | 1 | 2 | 3 | 4 | 5;
/**
* Style of card's inner content.
*/
contentStyle?: StyleProp;
- style?: Animated.WithAnimatedValue>;
+ style?: StyleProp>;
/**
* @optional
*/
@@ -91,6 +95,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 +149,7 @@ const Card = ({
...rest
}: (OutlinedCardProps | ElevatedCardProps | ContainedCardProps) & Props) => {
const theme = useInternalTheme(themeOverrides);
+
const isMode = React.useCallback(
(modeToCompare: Mode) => {
return cardMode === modeToCompare;
@@ -155,34 +164,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);
@@ -232,15 +230,12 @@ const Card = ({
return (
{isMode('outlined') && (
diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx
index b6482e9209..beeaaa29b3 100644
--- a/src/components/Chip/Chip.tsx
+++ b/src/components/Chip/Chip.tsx
@@ -1,21 +1,23 @@
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,
+ ViewProps,
ViewStyle,
} from 'react-native';
+import type { AnimatedStyle } from 'react-native-reanimated';
import useLatestCallback from 'use-latest-callback';
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';
@@ -25,7 +27,7 @@ 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 +125,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 +146,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 +207,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,25 +221,13 @@ 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 opacity = 0.38;
@@ -265,6 +257,7 @@ const Chip = ({
const elevationStyle = elevation;
const multiplier = compact ? 1.5 : 2;
+
const labelSpacings = {
marginRight: onClose ? 0 : 8 * multiplier,
marginLeft:
@@ -272,30 +265,25 @@ const Chip = ({
? 4 * multiplier
: 8 * multiplier,
};
+
const contentSpacings = {
paddingRight: onClose ? 34 : 0,
};
+
const labelTextStyle = {
color: textColor,
...theme.fonts.labelLarge,
};
+
return (
>;
+ 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..11d8cc6902 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,17 @@ import type {
ViewStyle,
} from 'react-native';
+import Animated, {
+ Easing,
+ ReduceMotion,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+ type AnimatedStyle,
+} 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';
@@ -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,12 @@ 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,8 +218,10 @@ 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);
@@ -258,7 +269,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 +315,23 @@ const Menu = ({
[anchor]
);
+ const handleShowAnimationFinished = useLatestCallback((finished: boolean) => {
+ if (finished) {
+ focusFirstDOMNode(menuRef.current);
+ }
+ });
+
+ const handleHideAnimationFinished = useLatestCallback((finished: boolean) => {
+ if (!finished) {
+ return;
+ }
+
+ setMenuLayout({ width: 0, height: 0 });
+ setRendered(false);
+ prevRendered.current = false;
+ focusFirstDOMNode(anchorRef.current);
+ });
+
const show = React.useCallback(async () => {
const windowLayoutResult = Dimensions.get('window');
const [menuLayoutResult, anchorLayoutResult] = await Promise.all([
@@ -343,46 +374,51 @@ const Menu = ({
width: windowLayoutResult.width,
});
+ prevRendered.current = true;
attachListeners();
+
requestAnimationFrame(() => {
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();
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) => {
@@ -403,8 +439,6 @@ const Menu = ({
);
React.useEffect(() => {
- const opacityAnimation = opacityAnimationRef.current;
- const scaleAnimation = scaleAnimationRef.current;
keyboardDidShowListenerRef.current = Keyboard.addListener(
'keyboardDidShow',
keyboardDidShow
@@ -418,26 +452,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 +484,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 +495,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 +583,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 +612,39 @@ const Menu = ({
}
}
- const shadowMenuContainerStyle = {
- opacity: opacityAnimationRef.current,
+ const shadowMenuContainerStyle: ViewStyle = {
+ borderRadius: theme.shapes.corner.extraSmall,
+ ...(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 +687,38 @@ const Menu = ({
>
- {(scrollableMenuHeight && (
-
- {children}
-
- )) || {children} }
+
+ {(scrollableMenuHeight && (
+
+ {children}
+
+ )) || {children} }
+
@@ -703,8 +736,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..8511bda46d 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 } 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,27 @@ 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<
+ AnimatedStyle>
+ >;
+ /**
+ * 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 +99,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 +132,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 +191,26 @@ function Modal({
'hardwareBackPress',
onHardwareBackPress
);
+
return () => subscription.remove();
}, [dismissable, dismissableBackButton, onDismissCallback, visible]);
- if (!visibleInternal) {
+ const transitionStyle: AnimatedStyle = {
+ transitionDuration: scale * DEFAULT_DURATION,
+ transitionProperty: 'opacity',
+ transitionTimingFunction: cubicBezier(1 / 3, 1, 2 / 3, 1),
+ };
+
+ 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 +229,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, transitionStyle]}
testID={`${testID}-backdrop`}
/>
{children}
@@ -238,9 +271,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..918cbcd398 100644
--- a/src/components/Searchbar.tsx
+++ b/src/components/Searchbar.tsx
@@ -1,5 +1,5 @@
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,
@@ -9,6 +9,8 @@ import type {
ViewStyle,
} from 'react-native';
+import type { AnimatedStyle } from 'react-native-reanimated';
+
import ActivityIndicator from './ActivityIndicator';
import Divider from './Divider';
import type { IconSource } from './Icon';
@@ -111,12 +113,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?: 0 | 1 | 2 | 3 | 4 | 5;
/**
* 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 +190,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 +234,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?: 0 | 1 | 2 | 3 | 4 | 5;
/**
* 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 ? (
@@ -382,10 +375,13 @@ const styles = StyleSheet.create({
width: '100%',
},
container: {
+ margin: 8,
+ minHeight: 48,
+ pointerEvents: 'box-none',
+ },
+ contentContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
- margin: 8,
- borderRadius: 4,
minHeight: 48,
},
content: {
diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx
index b0b0f0c4aa..7f1fce28c9 100644
--- a/src/components/Surface.tsx
+++ b/src/components/Surface.tsx
@@ -1,205 +1,137 @@
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 { StyleProp, ViewProps, ViewStyle } from 'react-native';
+
+import Animated, {
+ 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?: AnimatedStyleProp<'backgroundColor'>;
+ /**
+ * 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;
+ /**
+ * Radius of the bottom-right corner of the Surface.
+ */
+ borderBottomRightRadius?: BorderRadius;
+ /**
+ * Radius of the bottom-start corner of the Surface.
+ */
+ borderBottomStartRadius?: BorderRadius;
+ /**
+ * Radius of the end-end corner of the Surface.
+ */
+ borderEndEndRadius?: BorderRadius;
+ /**
+ * Radius of the end-start corner of the Surface.
+ */
+ borderEndStartRadius?: BorderRadius;
+ /**
+ * Radius of the start-end corner of the Surface.
+ */
+ borderStartEndRadius?: BorderRadius;
/**
- * Content of the `Surface`.
+ * Radius of the start-start corner of the Surface.
*/
- children: React.ReactNode;
- style?: Animated.WithAnimatedValue>;
+ borderStartStartRadius?: 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 top-end corner of the Surface.
*/
- elevation?: SurfaceElevation;
+ borderTopEndRadius?: 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 top-left corner of the Surface.
*/
- mode?: 'flat' | 'elevated';
+ borderTopLeftRadius?: BorderRadius;
/**
- * @optional
+ * Radius of the top-right corner of the Surface.
*/
- theme?: ThemeProp;
+ borderTopRightRadius?: BorderRadius;
/**
- * TestID used for testing purposes
+ * Radius of the top-start corner of the Surface.
*/
- testID?: string;
- ref?: React.Ref;
+ borderTopStartRadius?: BorderRadius;
/**
- * @internal
+ * Corner curve of the Surface on iOS.
*/
- container?: boolean;
+ borderCurve?: ViewStyle['borderCurve'];
};
-const outerLayerStyleProperties: (keyof ViewStyle)[] = [
- 'position',
- 'alignSelf',
- 'top',
- 'right',
- 'bottom',
- 'left',
- 'start',
- 'end',
- 'flex',
- 'flexShrink',
- 'flexGrow',
- 'width',
- 'height',
- 'transform',
- 'opacity',
-];
-
-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 & {
+ /**
+ * 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 +140,7 @@ const SurfaceIOS = ({
* import { StyleSheet } from 'react-native';
*
* const MyComponent = () => (
- *
+ *
* Surface
*
* );
@@ -217,9 +149,9 @@ const SurfaceIOS = ({
*
* const styles = StyleSheet.create({
* surface: {
- * padding: 8,
* height: 80,
* width: 80,
+ * padding: 8,
* alignItems: 'center',
* justifyContent: 'center',
* },
@@ -231,44 +163,86 @@ const Surface = ({
children,
theme: overridenTheme,
style,
- testID = 'surface',
+ backgroundColor: customBackgroundColor,
+ borderRadius,
+ borderBottomEndRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ borderBottomStartRadius,
+ borderEndEndRadius,
+ borderEndStartRadius,
+ borderStartEndRadius,
+ borderStartStartRadius,
+ borderTopEndRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderTopStartRadius,
+ borderCurve,
+ testID,
mode = 'elevated',
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 visualStyle = useAnimatedStyle(() =>
+ Object.fromEntries(
+ Object.entries({
+ backgroundColor,
+ borderRadius,
+ borderBottomEndRadius,
+ borderBottomLeftRadius,
+ borderBottomRightRadius,
+ borderBottomStartRadius,
+ borderEndEndRadius,
+ borderEndStartRadius,
+ borderStartEndRadius,
+ borderStartStartRadius,
+ borderTopEndRadius,
+ borderTopLeftRadius,
+ borderTopRightRadius,
+ borderTopStartRadius,
+ borderCurve,
+ }).map(([property, value]) => [
+ property,
+ isSharedValue(value) ? value.value : value,
+ ])
+ )
+ );
const isElevated = mode === 'elevated';
+ const transitionDuration = 150 * theme.animation.scale;
+
+ let transitionStyle: AnimatedStyle = {
+ transitionDuration,
+ transitionTimingFunction: 'ease-in-out',
+ };
+
if (Platform.OS === 'web') {
- const { pointerEvents = 'auto' } = props;
+ const [elevationShadow] = shadow(elevation, theme.colors.shadow);
+
+ transitionStyle = {
+ ...transitionStyle,
+ transitionProperty: 'boxShadow',
+ };
+
return (
{children}
@@ -277,40 +251,23 @@ 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];
+ transitionStyle = {
+ ...transitionStyle,
+ 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 +275,42 @@ const Surface = ({
);
}
+ const [spotShadow, ambientShadow] = shadow(elevation, theme.colors.shadow);
+
+ transitionStyle = {
+ ...transitionStyle,
+ transitionProperty: ['shadowOpacity', 'shadowOffset', 'shadowRadius'],
+ };
+
return (
-
+
{children}
-
+
);
};
+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..8ad6b28645 100644
--- a/src/components/__tests__/Appbar/Appbar.test.tsx
+++ b/src/components/__tests__/Appbar/Appbar.test.tsx
@@ -1,12 +1,8 @@
-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';
import { render, screen } from '../../../test-utils';
-import { tokens } from '../../../theme/tokens';
import Appbar from '../../Appbar';
import {
getAppbarBackgroundColor,
@@ -280,150 +276,83 @@ 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(
+ getTheme().colors.surfaceContainer
);
});
- 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(
+ getTheme(true).colors.surfaceContainer
);
});
});
-describe('animated value changes correctly', () => {
- it('appbar animated value changes correctly', async () => {
- const value = new Animated.Value(1);
+describe('style props', () => {
+ it('appbar style is applied correctly', async () => {
await render(
-
+
);
- 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 }],
+ expect(screen.getByTestId('appbar')).toHaveStyle({
+ marginTop: 12,
});
});
- it('action animated value changes correctly', async () => {
- const value = new Animated.Value(1);
+ it('action style is applied correctly', async () => {
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 }],
+ expect(screen.getByTestId('appbar-action-container')).toHaveStyle({
+ marginTop: 12,
});
});
- it('back action animated value changes correctly', async () => {
- const value = new Animated.Value(1);
+ it('back action style is applied correctly', async () => {
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 }],
+ expect(screen.getByTestId('appbar-back-action-container')).toHaveStyle({
+ marginTop: 12,
});
});
- it('header animated value changes correctly', async () => {
- const value = new Animated.Value(1);
+ it('header style is applied correctly', async () => {
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 }],
+ expect(screen.getByTestId('appbar-header')).toHaveStyle({
+ marginTop: 12,
});
});
@@ -437,7 +366,8 @@ describe('animated value changes correctly', () => {
);
- expect(screen.getByTestId('appbar-header-root-layer')).toHaveStyle(style);
+
+ expect(screen.getByTestId('appbar-header')).toHaveStyle(style);
});
describe('getAppbarBorders', () => {
diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
index a5d9d95766..db67c1b3b9 100644
--- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
+++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
@@ -3,34 +3,105 @@
exports[`Appbar does not pass any additional props to Searchbar 1`] = `
-
+
-
+
+
-
+
+
-
-
-
-
- magnify
-
-
-
-
+ ],
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ magnify
+
-
+
+
+
+
+
-
-
-
-
-
- close
-
-
-
-
+ ],
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ close
+
@@ -419,34 +769,105 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and AppbarAction 1`] = `
-
+
-
+
+
-
-
+
+
-
-
-
-
-
+ }
+ />
-
+
+
-
- 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..163fd6612a 100644
--- a/src/components/__tests__/BottomNavigation.test.tsx
+++ b/src/components/__tests__/BottomNavigation.test.tsx
@@ -601,7 +601,10 @@ it('barStyle animated value changes correctly', async () => {
barStyle={[{ transform: [{ scale: value }] }]}
/>
);
- expect(screen.getByTestId('bottom-navigation-bar-outer-layer')).toHaveStyle({
+
+ expect(
+ screen.getByTestId('bottom-navigation-bar-animated-wrapper')
+ ).toHaveStyle({
transform: [{ scale: 1 }],
});
@@ -614,7 +617,10 @@ 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-bar-animated-wrapper')
+ ).toHaveStyle({
transform: [{ scale: 1.5 }],
});
});
diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx
index 4da466837a..7612184e48 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';
@@ -709,33 +709,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..66450300ae 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 { StyleSheet, Text } 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';
@@ -224,32 +223,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..e4df7ba341 100644
--- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
+++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
@@ -3,28 +3,85 @@
exports[`Card renders an outlined card 1`] = `
-
+
-
-
-
+ "shadowOpacity": 0,
+ "shadowRadius": 0,
+ },
+ ]
+ }
+ style={
+ [
+ {
+ "bottom": 0,
+ "left": 0,
+ "position": "absolute",
+ "right": 0,
+ "top": 0,
+ },
+ {
+ "pointerEvents": "none",
+ },
+ {},
+ {
+ "backgroundColor": "rgba(254, 247, 255, 1)",
+ "borderBottomEndRadius": undefined,
+ "borderBottomLeftRadius": undefined,
+ "borderBottomRightRadius": undefined,
+ "borderBottomStartRadius": undefined,
+ "borderCurve": undefined,
+ "borderEndEndRadius": undefined,
+ "borderEndStartRadius": undefined,
+ "borderRadius": 12,
+ "borderStartEndRadius": undefined,
+ "borderStartStartRadius": undefined,
+ "borderTopEndRadius": undefined,
+ "borderTopLeftRadius": undefined,
+ "borderTopRightRadius": undefined,
+ "borderTopStartRadius": undefined,
+ },
+ {
+ "shadowColor": "rgba(0, 0, 0, 1)",
+ "shadowOffset": {
+ "height": 0,
+ "width": 0,
+ },
+ "shadowOpacity": 0,
+ "shadowRadius": 0,
+ },
+ ]
+ }
+ testID="card-container-shadow-layer"
+ />
+
+
`;
diff --git a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap
index 54f2e4f7a4..8e23dac3e3 100644
--- a/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap
+++ b/src/components/__tests__/Checkbox/__snapshots__/Checkbox.test.tsx.snap
@@ -83,6 +83,35 @@ exports[`renders Checkbox with custom testID 1`] = `
}
>
{
});
});
});
-
-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__/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..018e0c9b71 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';
@@ -210,8 +210,7 @@ it('respects anchorPosition bottom', async () => {
dimensionsSpy.mockRestore();
});
-it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
+it('applies content styles', async () => {
await render(
{
onDismiss={jest.fn()}
anchor={Open menu }
testID="menu"
- contentStyle={[{ transform: [{ scale: value }] }]}
+ contentStyle={{ marginTop: 12 }}
>
);
- 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 }],
+ expect(screen.getByTestId('menu-surface-content')).toHaveStyle({
+ marginTop: 12,
});
});
diff --git a/src/components/__tests__/Modal.test.tsx b/src/components/__tests__/Modal.test.tsx
index ff4cf2fd41..887f300458 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,
});
@@ -578,33 +576,19 @@ describe('Modal', () => {
});
});
- it('animated value changes correctly', async () => {
- const value = new Animated.Value(1);
+ it('applies content container style', async () => {
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 }],
+ expect(screen.getByTestId('modal-surface')).toHaveStyle({
+ marginTop: 12,
});
});
});
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..afa12480a0 100644
--- a/src/components/__tests__/Surface.test.tsx
+++ b/src/components/__tests__/Surface.test.tsx
@@ -1,6 +1,6 @@
-import type { ViewStyle } from 'react-native';
import { StyleSheet } from 'react-native';
import { Platform } from 'react-native';
+import type { ViewStyle } from 'react-native';
import {
afterEach,
@@ -15,14 +15,14 @@ import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
import Surface from '../Surface';
+const SPOT_SHADOW_OPACITY = 0.19;
+const AMBIENT_SHADOW_OPACITY = 0.039;
+
type StyleCase = {
property: keyof ViewStyle;
value: ViewStyle[keyof ViewStyle];
};
-const SPOT_SHADOW_OPACITY = 0.19;
-const AMBIENT_SHADOW_OPACITY = 0.039;
-
afterEach(() => {
jest.restoreAllMocks();
});
@@ -30,14 +30,11 @@ afterEach(() => {
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'
- );
+ expect(screen.getByLabelText('Surface')).toBeOnTheScreen();
});
describe('on iOS', () => {
@@ -46,43 +43,28 @@ describe('Surface', () => {
});
const styles = StyleSheet.create({
- absoluteStyles: {
- bottom: 10,
- end: 20,
- left: 30,
- position: 'absolute',
- right: 40,
- start: 50,
- top: 60,
- },
- innerLayerViewStyle: {
- padding: 13,
- },
- restStyle: {
+ surface: {
padding: 10,
flexDirection: 'row',
alignContent: 'center',
+ margin: 12,
+ width: 100,
},
});
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({
+ expect(screen.getByTestId('surface-test')).not.toHaveStyle({
shadowOpacity: expect.any(Number),
});
// @ts-expect-error
- expect(screen.getByTestId('surface-test')).not.toHaveStyle({
+ expect(screen.getByTestId('surface-test-shadow-layer')).not.toHaveStyle({
shadowOpacity: expect.any(Number),
});
expect(screen.getByTestId('surface-test')).toHaveStyle({
@@ -97,14 +79,27 @@ describe('Surface', () => {
);
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle({
+ expect(screen.getByTestId('surface-test')).toHaveStyle({
shadowOpacity: SPOT_SHADOW_OPACITY,
});
- expect(screen.getByTestId('surface-test')).toHaveStyle({
+ expect(screen.getByTestId('surface-test-shadow-layer')).toHaveStyle({
shadowOpacity: AMBIENT_SHADOW_OPACITY,
});
});
+ it('applies styles to the view that contains the children', async () => {
+ await render(
+
+ {null}
+
+ );
+
+ expect(screen.getByTestId('surface-test')).toHaveStyle(styles.surface);
+ expect(screen.getByTestId('surface-test-shadow-layer')).not.toHaveStyle(
+ styles.surface
+ );
+ });
+
it.each([
{ property: 'opacity', value: 0.7 },
{ property: 'transform', value: [{ scale: 1.02 }] },
@@ -126,25 +121,6 @@ describe('Surface', () => {
{ 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 },
@@ -155,7 +131,7 @@ describe('Surface', () => {
{ property: 'borderWidth', value: 2 },
{ property: 'borderColor', value: 'black' },
] satisfies StyleCase[])(
- 'applies $property to inner layer only',
+ 'applies $property to the view that contains the children',
async ({ property, value }) => {
const style = { [property]: value };
@@ -165,118 +141,32 @@ describe('Surface', () => {
);
- 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('should render absolute position properties on outer layer', async () => {
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(
- styles.absoluteStyles
- );
- });
-
- it('should render absolute position properties on the outer layer', async () => {
- await render(
-
- {null}
-
- );
-
- expect(screen.getByTestId('surface-test-outer-layer')).toHaveStyle(
- styles.absoluteStyles
- );
- });
- });
-
- 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 () => {
+ it('applies visual props to every shadow layer', async () => {
const backgroundColor = 'rgb(1, 2, 3)';
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);
- });
+ const visualStyle = {
+ backgroundColor,
+ borderRadius: 4,
+ borderTopLeftRadius: 8,
+ };
+ expect(screen.getByTestId('surface-test')).toHaveStyle(visualStyle);
+ expect(screen.getByTestId('surface-test-shadow-layer')).toHaveStyle(
+ visualStyle
+ );
});
});
@@ -287,12 +177,7 @@ describe('Surface', () => {
it('should render Surface with appropriate bg color but without shadow, if mode is set to "flat"', async () => {
await render(
-
+
{null}
);
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..0691cfec5b 100644
--- a/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Badge.test.tsx.snap
@@ -3,8 +3,18 @@
exports[`renders badge 1`] = `
`;
exports[`renders badge as hidden 1`] = `
3
@@ -80,8 +157,18 @@ exports[`renders badge as hidden 1`] = `
exports[`renders badge in different color 1`] = `
3
@@ -128,8 +251,18 @@ exports[`renders badge in different color 1`] = `
exports[`renders badge with content 1`] = `
3
diff --git a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap
index 4e75db8cc7..48880b3a64 100644
--- a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap
@@ -3,127 +3,415 @@
exports[`render visible banner, with custom theme 1`] = `
-
+
+
+
-
-
-
- Custom theme
-
-
+ ],
+ ]
+ }
+ >
+ Custom theme
+
+
+
-
+
-
+
-
+
+
-
-
- first
-
-
-
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ first
+
@@ -279,133 +647,341 @@ exports[`render visible banner, with custom theme 1`] = `
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.
+
+
@@ -414,149 +990,437 @@ exports[`renders hidden banner, without action buttons and without image 1`] = `
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.
-
-
+ ],
+ ]
+ }
+ >
+ Two line text string with two actions. One to two lines is preferable on mobile.
+
+
+
-
+
-
+
-
+
+
-
-
- first
-
-
-
+ undefined,
+ ],
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ first
+
@@ -712,127 +1656,415 @@ exports[`renders visible banner, with action buttons and with image 1`] = `
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.
-
-
+ ],
+ ]
+ }
+ >
+ 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
+
@@ -1140,143 +2627,336 @@ exports[`renders visible banner, with action buttons and without image 1`] = `
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.
+
+
@@ -1285,121 +2965,314 @@ exports[`renders visible banner, without action buttons and with image 1`] = `
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..d7043ac618 100644
--- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap
@@ -74,42 +74,209 @@ exports[`allows customizing Route's type via generics 1`] = `
+
@@ -525,8 +725,18 @@ exports[`allows customizing Route's type via generics 1`] = `
>
@@ -757,42 +990,209 @@ exports[`hides labels in non-shifting bottom navigation 1`] = `
+
@@ -1209,8 +1642,18 @@ exports[`hides labels in non-shifting bottom navigation 1`] = `
>
@@ -1410,8 +1876,18 @@ exports[`hides labels in non-shifting bottom navigation 1`] = `
>
@@ -1521,52 +2020,219 @@ exports[`hides labels in shifting bottom navigation 1`] = `
-
+
+
@@ -1973,8 +2672,18 @@ exports[`hides labels in shifting bottom navigation 1`] = `
>
@@ -2174,8 +2906,18 @@ exports[`hides labels in shifting bottom navigation 1`] = `
>
@@ -2423,42 +3188,209 @@ exports[`renders bottom navigation with getLazy 1`] = `
+
@@ -2994,8 +3959,18 @@ exports[`renders bottom navigation with getLazy 1`] = `
>
@@ -3315,8 +4313,18 @@ exports[`renders bottom navigation with getLazy 1`] = `
>
-
-
-
+
+
+
@@ -3957,8 +5021,18 @@ exports[`renders bottom navigation with getLazy 1`] = `
>
@@ -4189,42 +5286,209 @@ exports[`renders bottom navigation with scene animation 1`] = `
+
@@ -4714,8 +6011,18 @@ exports[`renders bottom navigation with scene animation 1`] = `
>
@@ -4984,8 +6314,18 @@ exports[`renders bottom navigation with scene animation 1`] = `
>
@@ -5254,8 +6617,18 @@ exports[`renders bottom navigation with scene animation 1`] = `
>
@@ -5524,8 +6920,18 @@ exports[`renders bottom navigation with scene animation 1`] = `
>
@@ -5700,42 +7129,209 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
+
@@ -6086,8 +7715,18 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
>
@@ -6282,8 +7944,18 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
>
@@ -6449,42 +8144,209 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
+
@@ -7000,8 +8928,18 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
>
@@ -7178,8 +9139,18 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
>
@@ -7356,8 +9350,18 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
>
@@ -7500,42 +9527,209 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
+
@@ -8071,8 +10298,18 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
>
@@ -8392,6 +10652,41 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
>
+
@@ -9149,8 +11642,18 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
>
@@ -9419,8 +11945,18 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
>
@@ -9595,42 +12154,209 @@ exports[`renders non-shifting bottom navigation 1`] = `
+
@@ -10146,26 +12905,61 @@ exports[`renders non-shifting bottom navigation 1`] = `
],
]
}
- >
- camera
-
-
-
-
+ camera
+
+
+
+
@@ -10719,42 +13544,209 @@ exports[`renders shifting bottom navigation 1`] = `
+
@@ -11244,8 +14269,18 @@ exports[`renders shifting bottom navigation 1`] = `
>
@@ -11514,8 +14572,18 @@ exports[`renders shifting bottom navigation 1`] = `
>
@@ -11784,8 +14875,18 @@ exports[`renders shifting bottom navigation 1`] = `
>
@@ -12054,8 +15178,18 @@ exports[`renders shifting bottom navigation 1`] = `
>
diff --git a/src/components/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/__tests__/__snapshots__/Button.test.tsx.snap
index bbc1fff1be..434dd772ef 100644
--- a/src/components/__tests__/__snapshots__/Button.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Button.test.tsx.snap
@@ -3,32 +3,97 @@
exports[`renders button with an accessibility hint 1`] = `
-
+
-
+
+
-
-
- Button with accessibility hint
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Button with accessibility hint
+
@@ -155,34 +323,99 @@ 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
+
@@ -309,150 +645,318 @@ exports[`renders button with an accessibility label 1`] = `
exports[`renders button with button color 1`] = `
-
-
+
+
+
-
-
- Custom Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Custom Button
+
@@ -461,32 +965,97 @@ exports[`renders button with button color 1`] = `
exports[`renders button with color 1`] = `
-
+
-
+
+
-
-
- Custom Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Custom Button
+
@@ -613,32 +1285,97 @@ exports[`renders button with color 1`] = `
exports[`renders button with custom testID 1`] = `
-
+
-
+
+
-
-
- Button with custom testID
-
-
+ ],
+ ]
+ }
+ testID="custom:testID-text"
+ >
+ Button with custom testID
+
@@ -765,199 +1605,367 @@ exports[`renders button with custom testID 1`] = `
exports[`renders button with icon 1`] = `
+ }
+ jestInlineStyle={
+ [
+ {
+ "transitionDuration": 150,
+ "transitionProperty": [
+ "shadowOpacity",
+ "shadowOffset",
+ "shadowRadius",
+ ],
+ "transitionTimingFunction": "ease-in-out",
+ },
+ [
+ {
+ "borderStyle": "solid",
+ "minWidth": 64,
+ },
+ undefined,
+ {
+ "borderColor": "transparent",
+ "borderWidth": 0,
+ },
+ undefined,
+ ],
+ {
+ "shadowColor": "rgba(0, 0, 0, 1)",
+ "shadowOffset": {
+ "height": 0,
+ "width": 0,
+ },
+ "shadowOpacity": 0,
+ "shadowRadius": 0,
+ },
+ ]
+ }
+ style={
+ [
+ {},
+ {
+ "borderStyle": "solid",
+ "minWidth": 64,
+ },
+ undefined,
+ {
+ "borderColor": "transparent",
+ "borderWidth": 0,
+ },
+ undefined,
+ {
+ "backgroundColor": "transparent",
+ "borderBottomEndRadius": undefined,
+ "borderBottomLeftRadius": undefined,
+ "borderBottomRightRadius": undefined,
+ "borderBottomStartRadius": undefined,
+ "borderCurve": undefined,
+ "borderEndEndRadius": undefined,
+ "borderEndStartRadius": undefined,
+ "borderRadius": 20,
+ "borderStartEndRadius": undefined,
+ "borderStartStartRadius": undefined,
+ "borderTopEndRadius": undefined,
+ "borderTopLeftRadius": undefined,
+ "borderTopRightRadius": undefined,
+ "borderTopStartRadius": undefined,
+ },
+ {
+ "shadowColor": "rgba(0, 0, 0, 1)",
+ "shadowOffset": {
+ "height": 0,
+ "width": 0,
+ },
+ "shadowOpacity": 0,
+ "shadowRadius": 0,
+ },
+ ]
+ }
+ testID="button-container"
+>
+
-
-
- camera
-
-
-
+ camera
+
+
+
- Icon Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Icon Button
+
@@ -966,32 +1974,97 @@ exports[`renders button with icon 1`] = `
exports[`renders button with icon in reverse order 1`] = `
-
+
-
+
+
-
-
- chevron-right
-
-
-
+ chevron-right
+
+
+
- Right Icon
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Right Icon
+
@@ -1169,32 +2345,97 @@ exports[`renders button with icon in reverse order 1`] = `
exports[`renders contained contained with mode 1`] = `
-
+
-
+
+
-
-
- Contained Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Contained Button
+
@@ -1322,32 +2666,97 @@ exports[`renders contained contained with mode 1`] = `
exports[`renders disabled button 1`] = `
-
+
-
+
+
-
-
- Disabled Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Disabled Button
+
@@ -1474,32 +2986,97 @@ exports[`renders disabled button 1`] = `
exports[`renders loading button 1`] = `
-
+
-
+
+
@@ -1610,13 +3305,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 +3319,8 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "rotate": "45deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
"width": 18,
}
}
@@ -1638,8 +3329,15 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
- "overflow": "hidden",
+ "height": 18,
+ "transform": [
+ {
+ "translateY": 0,
+ },
+ {
+ "rotate": "-165deg",
+ },
+ ],
"width": 18,
}
}
@@ -1648,15 +3346,8 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "translateY": 0,
- },
- {
- "rotate": "-165deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
"width": 18,
}
}
@@ -1665,40 +3356,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 +3401,9 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "rotate": "45deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
+ "top": 9,
"width": 18,
}
}
@@ -1720,9 +3412,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 +3429,8 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
- "transform": [
- {
- "translateY": -9,
- },
- {
- "rotate": "345deg",
- },
- ],
+ "height": 9,
+ "overflow": "hidden",
"width": 18,
}
}
@@ -1748,80 +3439,69 @@ 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
+
@@ -1830,32 +3510,97 @@ exports[`renders loading button 1`] = `
exports[`renders outlined button with mode 1`] = `
-
+
-
+
+
-
-
- Outlined Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Outlined Button
+
@@ -1983,32 +3831,97 @@ exports[`renders outlined button with mode 1`] = `
exports[`renders text button by default 1`] = `
-
+
-
+
+
-
-
- Text Button
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Text Button
+
@@ -2135,32 +4151,97 @@ exports[`renders text button by default 1`] = `
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..28c7006561 100644
--- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
@@ -3,32 +3,101 @@
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 +940,104 @@ 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..38f643c1c0 100644
--- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
@@ -381,281 +381,299 @@ exports[`DataTable.Pagination renders data table pagination 1`] = `
>
-
+
-
+
-
-
- chevron-left
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-left
+
-
+
-
+
-
-
- chevron-right
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-right
+
@@ -719,561 +737,597 @@ 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
+
@@ -1337,281 +1391,299 @@ exports[`DataTable.Pagination renders data table pagination with label 1`] = `
>
-
+
-
+
-
-
- chevron-left
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-left
+
-
+
-
+
-
-
- chevron-right
-
-
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ chevron-right
+
@@ -1683,33 +1755,59 @@ exports[`DataTable.Pagination renders data table pagination with options select
>
-
-
+
+
+
-
-
- menu-down
-
-
-
+ menu-down
+
+
+
- 2
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ 2
+
@@ -1923,561 +2167,597 @@ 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..00f077c522 100644
--- a/src/components/__tests__/__snapshots__/FAB.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/FAB.test.tsx.snap
@@ -2,15 +2,80 @@
exports[`renders FAB large size 1`] = `
+
+
-
-`;
-
+ style={
+ [
+ {
+ "borderWidth": 3,
+ "bottom": -5,
+ "left": -5,
+ "pointerEvents": "none",
+ "position": "absolute",
+ "right": -5,
+ "top": -5,
+ },
+ {
+ "borderColor": "rgba(98, 91, 113, 1)",
+ },
+ {
+ "borderRadius": 25,
+ "opacity": 0,
+ },
+ ]
+ }
+ />
+
+`;
+
exports[`renders FAB transitioning to not visible 1`] = `
+
-
+
+
+
+
+
-
+
+
@@ -1353,6 +3227,36 @@ exports[`renders FAB with default props 1`] = `
+
+
+`;
+
+exports[`renders FAB with secondary variant 1`] = `
+
+
-
-`;
-
-exports[`renders FAB with secondary variant 1`] = `
-
-
+
+
+`;
+
+exports[`renders FAB with tonalSecondary variant 1`] = `
+
-
-`;
-
-exports[`renders FAB with tonalSecondary variant 1`] = `
-
+
+
+
-
+
+
+
-
+
+
+
+
-