diff --git a/docs/docs/guides/03-icons.mdx b/docs/docs/guides/03-icons.mdx
index c773ce7a56..350b90d59c 100644
--- a/docs/docs/guides/03-icons.mdx
+++ b/docs/docs/guides/03-icons.mdx
@@ -27,7 +27,7 @@ You can pass the name of an icon from [`MaterialDesignIcons`](https://pictogramm
Example:
```js
-Press me
+
```
:::note
@@ -63,15 +63,14 @@ Remote image:
```js
- Press me
-
+ label="Press me"
+/>
```
Local image:
```js
-Press me
+
```
### 3. A render function
@@ -88,9 +87,8 @@ Example:
style={{ width: size, height: size, tintColor: color }}
/>
)}
->
- Press me
-
+ label="Press me"
+/>
```
### 4. Use custom icons
@@ -131,15 +129,14 @@ Example for using an image source:
},
direction: 'rtl',
}}
->
- Press me
-
+ label="Press me"
+/>
```
Example for using an icon name:
```js
-Press me
+
```
You can also use a render function. Along with `size` and `color`, you have access to `direction` which will either be `'rtl'` or `'ltr'`. You can then decide how to render your icon component accordingly.
@@ -163,7 +160,6 @@ Example of using a render function:
]}
/>
)}
->
- Press me
-
+ label="Press me"
+/>
```
diff --git a/docs/docs/guides/05-react-native-web.md b/docs/docs/guides/05-react-native-web.md
index af0331a7d9..d9b69b8250 100644
--- a/docs/docs/guides/05-react-native-web.md
+++ b/docs/docs/guides/05-react-native-web.md
@@ -4,279 +4,39 @@ title: Using on the Web
# Using on the Web
-## Pre-requisites
+React Native Paper supports web via [React Native for Web](https://necolas.github.io/react-native-web/), which lets you run React Native components in a browser using React DOM.
-Make sure that you have followed the getting started guide and have `react-native-paper` installed and configured before following this guide.
+Before continuing, make sure you have React Native Paper installed and configured by following the [Getting Started guide](getting-started.md).
-We're going to use [react-native-web](https://github.com/necolas/react-native-web) and [webpack](https://webpack.js.org/) to use React Native Paper on the web, so let's install them as well.
+## Setting up web support with Expo
-To install `react-native-web`, run:
+The recommended way to run React Native Paper on the web is with Expo, which has built-in web support via React Native for Web. Install the required dependencies:
-```bash npm2yarn
-npm install react-native-web react-dom react-art
+```bash
+npx expo install react-dom react-native-web @expo/metro-runtime
```
-### Using CRA ([Create React App](https://github.com/facebook/create-react-app))
+Then start the web server:
-Install [`react-app-rewired`](https://github.com/timarney/react-app-rewired) to override `webpack` configuration:
-
-```bash npm2yarn
-npm install --save-dev react-app-rewired
-```
-
-[Configure `babel-loader`](#2-configure-babel-loader) using a new file called `config-overrides.js`:
-
-```js
-module.exports = function override(config, env) {
- config.module.rules.push({
- test: /\.js$/,
- exclude: /node_modules[/\\](?!react-native-vector-icons)/,
- use: {
- loader: 'babel-loader',
- options: {
- // Disable reading babel configuration
- babelrc: false,
- configFile: false,
-
- // The configuration for compilation
- presets: [
- ['@babel/preset-env', { useBuiltIns: 'usage' }],
- '@babel/preset-react',
- '@babel/preset-flow',
- '@babel/preset-typescript',
- ],
- plugins: [
- '@babel/plugin-proposal-class-properties',
- '@babel/plugin-proposal-object-rest-spread',
- ],
- },
- },
- });
-
- return config;
-};
-```
-
-Change your script in `package.json`:
-
-```diff
-/* package.json */
-
- "scripts": {
-- "start": "react-scripts start",
-+ "start": "react-app-rewired start",
-- "build": "react-scripts build",
-+ "build": "react-app-rewired build",
-- "test": "react-scripts test --env=jsdom",
-+ "test": "react-app-rewired test --env=jsdom"
-}
-```
-
-### Custom webpack setup
-
-To install `webpack`, run:
-
-```bash npm2yarn
-npm install --save-dev webpack webpack-cli webpack-dev-server
-```
-
-If you don't have a webpack config in your project, copy the following to `webpack.config.js` get started:
-
-```js
-const path = require('path');
-
-module.exports = {
- mode: 'development',
-
- // Path to the entry file, change it according to the path you have
- entry: path.join(__dirname, 'App.js'),
-
- // Path for the output files
- output: {
- path: path.join(__dirname, 'dist'),
- filename: 'app.bundle.js',
- },
-
- // Enable source map support
- devtool: 'source-map',
-
- // Loaders and resolver config
- module: {
- rules: [],
- },
- resolve: {},
-
- // Development server config
- devServer: {
- contentBase: [path.join(__dirname, 'public')],
- historyApiFallback: true,
- },
-};
-```
-
-Also create a folder named `public` and add the following file named `index.html`:
-
-```html
-
-
-
-
-
-
-
- App
-
-
-
-
-
-
-
-```
-
-Now we're ready to start configuring the project.
-
-## Configure webpack
-
-### 1. Alias `react-native` to `react-native-web`
-
-First, we have to tell webpack to use `react-native-web` instead of `react-native`. Add the following alias in your webpack config under `resolve`:
-
-```js
-alias: {
- 'react-native$': require.resolve('react-native-web'),
-}
-```
-
-### 2. Configure `babel-loader`
-
-Next, we want to tell `babel-loader` to compile `react-native-paper` and `react-native-vector-icons`. We would also want to disable reading the babel configuration files to prevent any conflicts.
-
-First install the required dependencies:
-
-```bash npm2yarn
-npm install --save-dev babel-loader @babel/preset-env @babel/preset-react @babel/preset-flow @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
-```
-
-Now, add the following in the `module.rules` array in your webpack config:
-
-```js
-{
- test: /\.js$/,
- exclude: /node_modules[/\\](?!react-native-vector-icons)/,
- use: {
- loader: 'babel-loader',
- options: {
- // Disable reading babel configuration
- babelrc: false,
- configFile: false,
-
- // The configuration for compilation
- presets: [
- ['@babel/preset-env', { useBuiltIns: 'usage' }],
- '@babel/preset-react',
- '@babel/preset-flow',
- "@babel/preset-typescript"
- ],
- plugins: [
- '@babel/plugin-proposal-class-properties',
- '@babel/plugin-proposal-object-rest-spread'
- ],
- },
- },
-},
+```bash
+npx expo start --web
```
-### 3. Configure `file-loader`
+No additional bundler configuration is required. See the [Expo Web docs](https://docs.expo.dev/workflow/web/) for details on how Expo configures React Native for Web under the hood.
-#### webpack < 5.0
+## Without Expo
-To be able to import images and other assets using `require`, we need to configure `file-loader`. Let's install it:
-
-```bash npm2yarn
-npm install --save-dev file-loader
-```
+If you're not using Expo, follow the [React Native for Web setup guide](https://necolas.github.io/react-native-web/docs/setup/) to configure your bundler. The setup covers aliasing `react-native` to `react-native-web` in webpack, Babel, and Jest.
-To configure it, add the following in the `module.rules` array in your webpack config:
+You will also need to manually load the Material Design icon font used by Paper. Add the following to your HTML shell or inject it at the root of your app:
-```js
-{
- test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
- loader: 'file-loader',
+```css
+@font-face {
+ font-family: 'MaterialDesignIcons';
+ src: url('~@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf') format('truetype');
}
```
-##### webpack >= 5.0
-
-Use `asset/resource`, since `file-loader` was deprecated in webpack v5.
-
-```js
-{
- test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
- type: 'asset/resource'
-}
-```
-
-## Load the Material Design Icons
-
-If you followed the getting started guide, you should have the following code in your root component:
-
-```js
-
-
-
-```
-
-Now we need tweak this section to load the Material Design Icons from the [`react-native-vector-icons`](https://github.com/oblador/react-native-vector-icons) library:
-
-```js
-
-
- {Platform.OS === 'web' ? (
-
- ) : null}
-
-
-
-```
-
-Remember to import `Platform` from `react-native` at the top:
-
-```js
-import { Platform } from 'react-native';
-```
-
-You can also load these fonts using [`css-loader`](https://github.com/webpack-contrib/css-loader) if you prefer.
-
-## Load the Roboto fonts (optional)
-
-The default theme in React Native Paper uses the Roboto font. You can add them to your project following [the instructions on its Google Fonts page](https://fonts.google.com/specimen/Roboto?selection.family=Roboto:100,300,400,500).
-
-## We're done!
-
-You can run `webpack-dev-server` to run the webpack server and open your project in the browser. You can add the following script in your `package.json` under the `"scripts"` section to make it easier:
-
-```json
-"web": "webpack-dev-server --open"
-```
+## Load the Roboto font (optional)
-Now you can run `yarn web` to run the project on web.
+The default Paper theme uses the Roboto typeface. With Expo, use the [`@expo-google-fonts/roboto`](https://github.com/expo/google-fonts/tree/master/font-packages/roboto) package. For other setups, follow the instructions on the [Roboto specimen page](https://fonts.google.com/specimen/Roboto).
diff --git a/docs/docs/guides/09-react-navigation.md b/docs/docs/guides/09-react-navigation.md
index 5ea5d556fa..1213f98c35 100644
--- a/docs/docs/guides/09-react-navigation.md
+++ b/docs/docs/guides/09-react-navigation.md
@@ -86,9 +86,11 @@ function HomeScreen({ navigation }) {
return (
Home Screen
- navigation.navigate('Details')}>
- Go to details
-
+ navigation.navigate('Details')}
+ label="Go to details"
+ />
);
}
diff --git a/docs/docs/guides/11-ripple-effect.md b/docs/docs/guides/11-ripple-effect.md
index 31845060d7..d4501643e6 100644
--- a/docs/docs/guides/11-ripple-effect.md
+++ b/docs/docs/guides/11-ripple-effect.md
@@ -18,10 +18,10 @@ The `rippleColor` prop is available for every pressable component which allows y
console.log('Pressed')}>
- Press me
-
+ mode="filled"
+ onPress={() => console.log('Pressed')}
+ label="Press me"
+/>
```
## Disable ripple effect in all components
diff --git a/docs/src/components/BannerExample.tsx b/docs/src/components/BannerExample.tsx
index 59ec60634a..c5d0c7df09 100644
--- a/docs/src/components/BannerExample.tsx
+++ b/docs/src/components/BannerExample.tsx
@@ -74,15 +74,14 @@ const BannerExample = () => {
>
- {}}>
- Loading
-
- {}}>
- Icon
-
- {}}>
- Press me
-
+ {}} label="Loading" />
+ {}} label="Icon" />
+ {}}
+ label="Press me"
+ />
{}} />
{}} />
{}} />
diff --git a/docs/src/components/GetStartedButtons.tsx b/docs/src/components/GetStartedButtons.tsx
index fd00420b46..72aef205ab 100644
--- a/docs/src/components/GetStartedButtons.tsx
+++ b/docs/src/components/GetStartedButtons.tsx
@@ -36,9 +36,12 @@ const GetStartedButton = () => {
return (
-
- Get started
-
+
{
'https://snack.expo.dev/@react-native-paper/react-native-paper-example_v5'
)
}
- >
- Try on Snack
-
+ label="Try on Snack"
+ />
);
};
diff --git a/docs/src/data/screenshots.js b/docs/src/data/screenshots.js
index c1afa99a6a..bc20787a92 100644
--- a/docs/src/data/screenshots.js
+++ b/docs/src/data/screenshots.js
@@ -22,11 +22,11 @@ const screenshots = {
BottomNavigation: 'screenshots/bottom-navigation.gif',
'BottomNavigation.Bar': 'screenshots/bottom-navigation-tabs.jpg',
Button: {
- text: 'screenshots/button-1.png',
- outlined: 'screenshots/button-2.png',
- contained: 'screenshots/button-3.png',
+ filled: 'screenshots/button-3.png',
+ tonal: 'screenshots/button-5.png',
elevated: 'screenshots/button-4.png',
- 'contained-tonal': 'screenshots/button-5.png',
+ outlined: 'screenshots/button-2.png',
+ text: 'screenshots/button-1.png',
},
Card: {
elevated: 'screenshots/card-1.png',
diff --git a/docs/src/data/themeColors.js b/docs/src/data/themeColors.js
index f12b955341..55f531c45f 100644
--- a/docs/src/data/themeColors.js
+++ b/docs/src/data/themeColors.js
@@ -47,20 +47,20 @@ const themeColors = {
},
Button: {
active: {
- elevated: {
- backgroundColor: 'theme.colors.elevation.level1',
- textColor: 'theme.colors.primary',
- },
- contained: {
+ filled: {
backgroundColor: 'theme.colors.primary',
textColor: 'theme.colors.onPrimary',
},
- 'contained-tonal': {
+ tonal: {
backgroundColor: 'theme.colors.secondaryContainer',
textColor: 'theme.colors.onSecondaryContainer',
},
- outlined: {
+ elevated: {
+ backgroundColor: 'theme.colors.elevation.level1',
textColor: 'theme.colors.primary',
+ },
+ outlined: {
+ textColor: 'theme.colors.onSurfaceVariant',
borderColor: 'theme.colors.outline',
},
text: {
@@ -68,15 +68,15 @@ const themeColors = {
},
},
disabled: {
- elevated: {
+ filled: {
backgroundColor: 'theme.colors.surfaceDisabled',
textColor: 'theme.colors.onSurfaceDisabled',
},
- contained: {
+ tonal: {
backgroundColor: 'theme.colors.surfaceDisabled',
textColor: 'theme.colors.onSurfaceDisabled',
},
- 'contained-tonal': {
+ elevated: {
backgroundColor: 'theme.colors.surfaceDisabled',
textColor: 'theme.colors.onSurfaceDisabled',
},
diff --git a/docs/src/utils/__tests__/themeColors.test.tsx b/docs/src/utils/__tests__/themeColors.test.tsx
index 4803ea7a6b..668f1a0bbd 100644
--- a/docs/src/utils/__tests__/themeColors.test.tsx
+++ b/docs/src/utils/__tests__/themeColors.test.tsx
@@ -2,20 +2,20 @@ import { getMaxNestedLevel, getUniqueNestedKeys } from '../themeColors';
const Button = {
active: {
- elevated: {
- backgroundColor: 'theme.colors.elevation.level1',
- color: 'theme.colors.primary',
- },
- contained: {
+ filled: {
backgroundColor: 'theme.colors.primary',
color: 'theme.colors.onPrimary',
},
- 'contained-tonal': {
+ tonal: {
backgroundColor: 'theme.colors.secondaryContainer',
color: 'theme.colors.onSecondaryContainer',
},
- outlined: {
+ elevated: {
+ backgroundColor: 'theme.colors.elevation.level1',
color: 'theme.colors.primary',
+ },
+ outlined: {
+ color: 'theme.colors.onSurfaceVariant',
borderColor: 'theme.colors.outline',
},
text: {
@@ -23,15 +23,15 @@ const Button = {
},
},
disabled: {
- elevated: {
+ filled: {
backgroundColor: 'theme.colors.surfaceDisabled',
color: 'theme.colors.onSurfaceDisabled',
},
- contained: {
+ tonal: {
backgroundColor: 'theme.colors.surfaceDisabled',
color: 'theme.colors.onSurfaceDisabled',
},
- 'contained-tonal': {
+ elevated: {
backgroundColor: 'theme.colors.surfaceDisabled',
color: 'theme.colors.onSurfaceDisabled',
},
diff --git a/example/src/DrawerItems.tsx b/example/src/DrawerItems.tsx
index f9df80433c..fcfe677a2b 100644
--- a/example/src/DrawerItems.tsx
+++ b/example/src/DrawerItems.tsx
@@ -270,7 +270,7 @@ function DrawerItems() {
example directory.
- Ok
+
diff --git a/example/src/Examples/ButtonExample.tsx b/example/src/Examples/ButtonExample.tsx
index b56b0a8327..f4b3e8683b 100644
--- a/example/src/Examples/ButtonExample.tsx
+++ b/example/src/Examples/ButtonExample.tsx
@@ -1,349 +1,334 @@
import * as React from 'react';
import { Image, StyleSheet, View } from 'react-native';
-import { Button, List, Text, useTheme } from 'react-native-paper';
+import { Button, Chip, List, Switch, Text, useTheme } from 'react-native-paper';
import ScreenWrapper from '../ScreenWrapper';
+type Mode = 'text' | 'outlined' | 'elevated' | 'filled' | 'tonal';
+type SizeOption =
+ | 'unset'
+ | 'extra-small'
+ | 'small'
+ | 'medium'
+ | 'large'
+ | 'extra-large';
+type ShapeOption = 'unset' | 'round' | 'square';
+type IconPosition = 'leading' | 'trailing';
+
+const MODES: Mode[] = ['filled', 'tonal', 'elevated', 'outlined', 'text'];
+const SIZES: SizeOption[] = [
+ 'unset',
+ 'extra-small',
+ 'small',
+ 'medium',
+ 'large',
+ 'extra-large',
+];
+const SHAPES: ShapeOption[] = ['unset', 'round', 'square'];
+const ICON_POSITIONS: IconPosition[] = ['leading', 'trailing'];
+
+function OptionRow({
+ label,
+ value,
+ options,
+ onChange,
+}: {
+ label: string;
+ value: T;
+ options: readonly T[];
+ onChange: (value: T) => void;
+}) {
+ return (
+
+
+ {label}
+
+
+ {options.map((option) => (
+ onChange(option)}
+ style={styles.chip}
+ >
+ {option}
+
+ ))}
+
+
+ );
+}
+
+const SwitchRow = ({
+ label,
+ value,
+ onValueChange,
+}: {
+ label: string;
+ value: boolean;
+ onValueChange: (value: boolean) => void;
+}) => (
+
+ {label}
+
+
+);
+
const ButtonExample = () => {
const theme = useTheme();
-
const color = theme.colors.inversePrimary;
+ // Playground state.
+ const [mode, setMode] = React.useState('filled');
+ const [size, setSize] = React.useState('unset');
+ const [shape, setShape] = React.useState('unset');
+ const [iconPosition, setIconPosition] =
+ React.useState('leading');
+ const [showIcon, setShowIcon] = React.useState(false);
+ const [disabled, setDisabled] = React.useState(false);
+ const [loading, setLoading] = React.useState(false);
+ const [selected, setSelected] = React.useState(false);
+ const [compact, setCompact] = React.useState(false);
+
+ // Selected state for the static toggle showcase below.
+ const [selectedToggles, setSelectedToggles] = React.useState<
+ Record
+ >({});
+ const toggle = (key: string) =>
+ setSelectedToggles((prev) => ({ ...prev, [key]: !prev[key] }));
+
return (
-
-
- {}} style={styles.button}>
- Default
-
- {}} style={styles.button}>
- Custom
-
- {}} style={styles.button}>
- Disabled
-
- {}} style={styles.button}>
- Icon
-
- {}} style={styles.button}>
- Loading
-
+
+
{}}
- style={styles.button}
- contentStyle={styles.flexReverse}
- >
- Icon right
-
+ label="Play me"
+ />
+
+
+
+
+
+
+ {showIcon && (
+
+ )}
+
+
+
+ {/* `compact` is a no-op once a size is set, so only offer it for unset. */}
+ {size === 'unset' && (
+
+ )}
-
+
+
- {}}
- style={styles.button}
- >
- Default
-
- {}}
- style={styles.button}
- >
- Custom
-
- {}}
- style={styles.button}
- >
- Disabled
-
- {}}
- style={styles.button}
- >
- Icon
-
- {}}
- style={styles.button}
- >
- Loading
-
- {}}
- style={styles.button}
- contentStyle={styles.flexReverse}
- >
- Icon right
-
+ {MODES.map((m) => (
+ {}}
+ style={styles.button}
+ label={m}
+ />
+ ))}
-
+
+
- {}} style={styles.button}>
- Default
-
{}}
style={styles.button}
- >
- Custom
-
+ label="Enabled"
+ />
{}}
style={styles.button}
- >
- Disabled
-
+ label="Disabled"
+ />
{}}
- style={styles.button}
- >
- Icon
-
- {}}
style={styles.button}
- >
- Loading
-
- {}}
- style={styles.button}
- contentStyle={styles.flexReverse}
- >
- Icon right
-
+ label="Loading"
+ />
-
+
+
- {}} style={styles.button}>
- Default
-
- {}}
- style={styles.button}
- >
- Custom
-
- {}}
- style={styles.button}
- >
- Disabled
-
- {}}
- style={styles.button}
- >
- Icon
-
- {}}
- style={styles.button}
- >
- Loading
-
- {}}
- style={styles.button}
- contentStyle={styles.flexReverse}
- >
- Icon right
-
+ {SIZES.filter(
+ (s): s is Exclude => s !== 'unset'
+ ).map((s) => (
+ {}}
+ style={styles.button}
+ label={s}
+ />
+ ))}
-
+
+
+ {(['round', 'square'] as const).map((shapeVariant) => (
+
+ {(['extra-small', 'small', 'medium', 'large'] as const).map((s) => (
+ {}}
+ style={styles.button}
+ label={`${s} ${shapeVariant}`}
+ />
+ ))}
+
+ ))}
+
+
+
- {}} style={styles.button}>
- Default
-
- {}}
- style={styles.button}
- >
- Custom
-
- {}}
- style={styles.button}
- >
- Disabled
-
- {}}
- style={styles.button}
- >
- Icon
-
- {}}
- style={styles.button}
- >
- Loading
-
- {}}
- style={styles.button}
- contentStyle={styles.flexReverse}
- >
- Icon right
-
+ {(['outlined', 'text', 'tonal'] as const).map((m) => {
+ const key = `toggle-${m}`;
+ const isSelected = !!selectedToggles[key];
+ return (
+ toggle(key)}
+ style={styles.button}
+ icon={isSelected ? 'check' : 'plus'}
+ label={m}
+ />
+ );
+ })}
+
{}}
style={styles.button}
- >
- Remote image
-
+ label="Custom color"
+ />
{}}
style={styles.button}
- >
- Required asset
-
+ label="Remote image"
+ />
(
+ icon={({ size: iconSize }) => (
)}
onPress={() => {}}
style={styles.button}
- >
- Custom component
-
+ label="Custom component"
+ />
{}}
style={styles.button}
- labelStyle={[styles.fontStyles, styles.md3FontStyles]}
- >
- Custom Font
-
- {}} style={styles.button}>
- Custom text
-
+ labelStyle={styles.fontStyles}
+ label="Custom font"
+ />
{}}
style={styles.customRadius}
- >
- Custom radius
-
- {}} style={styles.noRadius}>
- Without radius
-
- {}}
- style={{ borderRadius: styles.customRadiusAndPadding.borderRadius }}
- contentStyle={styles.customRadiusAndPadding}
- >
- Custom radius and padding
-
-
-
-
- {}}
- style={styles.flexGrow1Button}
- >
- flex-grow: 1
-
+ label="Custom radius"
+ />
{}}
- style={styles.width100PercentButton}
- >
- width: 100%
-
-
-
-
-
- {(
- [
- 'text',
- 'outlined',
- 'contained',
- 'elevated',
- 'contained-tonal',
- ] as const
- ).map((mode) => {
- return (
- {}}
- style={styles.button}
- icon="camera"
- >
- Compact {mode}
-
- );
- })}
+ style={styles.fullWidthButton}
+ label="width: 100%"
+ />
@@ -353,6 +338,34 @@ const ButtonExample = () => {
ButtonExample.title = 'Button';
const styles = StyleSheet.create({
+ preview: {
+ minHeight: 160,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 16,
+ },
+ optionRow: {
+ paddingHorizontal: 16,
+ paddingVertical: 4,
+ },
+ optionLabel: {
+ marginBottom: 8,
+ },
+ chips: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ },
+ chip: {
+ marginBottom: 4,
+ },
+ switchRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
row: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -363,37 +376,20 @@ const styles = StyleSheet.create({
button: {
margin: 4,
},
- flexReverse: {
- flexDirection: 'row-reverse',
- },
- md3FontStyles: {
- lineHeight: 32,
- },
fontStyles: {
fontWeight: '800',
- fontSize: 24,
- },
- flexGrow1Button: {
- flexGrow: 1,
- marginTop: 10,
- },
- width100PercentButton: {
- width: '100%',
- marginTop: 10,
+ fontSize: 20,
},
customRadius: {
+ margin: 4,
borderTopLeftRadius: 16,
borderTopRightRadius: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 16,
},
- noRadius: {
- borderRadius: 0,
- },
- customRadiusAndPadding: {
- borderRadius: 4,
- paddingHorizontal: 12,
- paddingVertical: 6,
+ fullWidthButton: {
+ width: '100%',
+ marginTop: 10,
},
});
diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx
index ad3451e8b0..9a7973a8de 100644
--- a/example/src/Examples/CardExample.tsx
+++ b/example/src/Examples/CardExample.tsx
@@ -75,8 +75,8 @@ const CardExample = () => {
- {}}>Share
- {}}>Explore
+ {}} label="Share" />
+ {}} label="Explore" />
@@ -104,12 +104,8 @@ const CardExample = () => {
/>
- {}}>
- Share
-
- {}}>
- Explore
-
+ {}} label="Share" />
+ {}} label="Explore" />
{
mode="outlined"
onPress={_toggleDialog('dialog1')}
style={styles.button}
- >
- Long text
-
+ label="Long text"
+ />
- Radio buttons
-
+ label="Radio buttons"
+ />
- Progress indicator
-
+ label="Progress indicator"
+ />
- Undismissable Dialog
-
+ label="Undismissable Dialog"
+ />
- Custom colors
-
+ label="Custom colors"
+ />
- With icon
-
+ label="With icon"
+ />
{Platform.OS === 'android' && (
- Dismissable back button
-
+ label="Dismissable back button"
+ />
)}
-
- Ok
-
+
diff --git a/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx b/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx
index 21f647e75c..aa0b93f4ea 100644
--- a/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx
+++ b/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx
@@ -26,10 +26,8 @@ const DialogWithDismissableBackButton = ({
-
- Disagree
-
- Agree
+
+
diff --git a/example/src/Examples/Dialogs/DialogWithIcon.tsx b/example/src/Examples/Dialogs/DialogWithIcon.tsx
index f12089193d..b4112642d7 100644
--- a/example/src/Examples/Dialogs/DialogWithIcon.tsx
+++ b/example/src/Examples/Dialogs/DialogWithIcon.tsx
@@ -24,10 +24,12 @@ const DialogWithIcon = ({
-
- Disagree
-
- Agree
+
+
diff --git a/example/src/Examples/Dialogs/DialogWithLongText.tsx b/example/src/Examples/Dialogs/DialogWithLongText.tsx
index 3f826c1b40..e92bbe3267 100644
--- a/example/src/Examples/Dialogs/DialogWithLongText.tsx
+++ b/example/src/Examples/Dialogs/DialogWithLongText.tsx
@@ -63,7 +63,7 @@ const DialogWithLongText = ({
- Ok
+
diff --git a/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx b/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx
index 966422369e..07aca7ecf2 100644
--- a/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx
+++ b/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx
@@ -84,8 +84,8 @@ const DialogWithRadioBtns = ({ visible, close }: Props) => {
- Cancel
- Ok
+
+
diff --git a/example/src/Examples/Dialogs/UndismissableDialog.tsx b/example/src/Examples/Dialogs/UndismissableDialog.tsx
index 3a9fe10cb8..9a5ab75d9c 100644
--- a/example/src/Examples/Dialogs/UndismissableDialog.tsx
+++ b/example/src/Examples/Dialogs/UndismissableDialog.tsx
@@ -18,10 +18,8 @@ const UndismissableDialog = ({
This is an undismissable dialog!!
-
- Disagree
-
- Agree
+
+
diff --git a/example/src/Examples/MenuExample.tsx b/example/src/Examples/MenuExample.tsx
index a263b7ee39..4e720fcb6c 100644
--- a/example/src/Examples/MenuExample.tsx
+++ b/example/src/Examples/MenuExample.tsx
@@ -84,9 +84,11 @@ const MenuExample = ({ navigation }: Props) => {
visible={_getVisible('menu2')}
onDismiss={_toggleMenu('menu2')}
anchor={
-
- Menu with icons
-
+
}
>
{}} title="Undo" />
@@ -143,9 +145,11 @@ const MenuExample = ({ navigation }: Props) => {
onDismiss={_toggleMenu('menu5')}
anchorPosition="bottom"
anchor={
-
- Menu with anchor position bottom
-
+
}
>
{}} title="Item 1" />
@@ -159,9 +163,11 @@ const MenuExample = ({ navigation }: Props) => {
visible={_getVisible('menu4')}
onDismiss={_toggleMenu('menu4')}
anchor={
-
- Menu at bottom
-
+
}
>
{}} title="Bottom Item 1" />
diff --git a/example/src/Examples/ProgressBarExample.tsx b/example/src/Examples/ProgressBarExample.tsx
index 5b59566d6e..214f82b3ca 100644
--- a/example/src/Examples/ProgressBarExample.tsx
+++ b/example/src/Examples/ProgressBarExample.tsx
@@ -41,11 +41,12 @@ const ProgressBarExample = () => {
return (
- setVisible(!visible)}>Toggle visibility
- setProgress(Math.random())}>
- Random progress
-
- Toggle animation
+ setVisible(!visible)} label="Toggle visibility" />
+ setProgress(Math.random())}
+ label="Random progress"
+ />
+
Default ProgressBar
diff --git a/example/src/Examples/SnackbarExample.tsx b/example/src/Examples/SnackbarExample.tsx
index e31955f18c..06e65ce077 100644
--- a/example/src/Examples/SnackbarExample.tsx
+++ b/example/src/Examples/SnackbarExample.tsx
@@ -90,9 +90,8 @@ const SnackbarExample = () => {
onPress={() =>
setOptions({ ...options, showSnackbar: !showSnackbar })
}
- >
- {showSnackbar ? 'Hide' : 'Show'}
-
+ label={showSnackbar ? 'Hide' : 'Show'}
+ />
{
- {}}>Share
- {}}>Read more
+ {}} label="Share" />
+ {}} label="Read more" />
@@ -124,8 +124,8 @@ const News = () => {
- {}}>Share
- {}}>Read more
+ {}} label="Share" />
+ {}} label="Read more" />
diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx
index b1a00b4ce4..428a225d37 100644
--- a/src/components/Banner.tsx
+++ b/src/components/Banner.tsx
@@ -246,10 +246,9 @@ const Banner = ({
style={styles.button}
textColor={colors.primary}
theme={theme}
+ label={label}
{...others}
- >
- {label}
-
+ />
))}
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 17d40fb035..6349184143 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -15,10 +15,18 @@ import {
import {
ButtonMode,
+ ButtonShape,
+ ButtonSize,
getButtonColors,
+ getButtonIconStyle,
+ getButtonRippleColor,
+ getButtonShapeRadius,
+ getButtonSizeStyle,
getButtonTouchableRippleStyle,
} from './utils';
+import { getDefaultDirection, useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
+import { toRawSpring } from '../../theme/tokens/sys/motion';
import type { $Omit, Theme, ThemeProp } from '../../types';
import { forwardRef } from '../../utils/forwardRef';
import hasTouchHandler from '../../utils/hasTouchHandler';
@@ -31,26 +39,57 @@ import TouchableRipple, {
} from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
-export type Props = $Omit, 'mode'> & {
+export type Props = $Omit<
+ React.ComponentProps,
+ 'mode' | 'children'
+> & {
/**
- * 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.
+ * Mode of the button. You can change the mode to adjust the styling to give it desired emphasis. Defaults to `filled`.
+ * - `filled` - button with a background color, used for the most important action, has the most visual impact and high emphasis. (default)
+ * - `tonal` - button with a secondary background color, an alternative middle ground between filled and outlined buttons.
+ * - `elevated` - button with a background color and elevation, used when absolutely necessary e.g. button requires visual separation from a patterned background.
* - `outlined` - button with an outline without background, typically used for important, but not primary action – represents medium emphasis.
- * - `contained` - button with a background color, used for important action, have the most visual impact and high emphasis.
- * - `elevated` - button with a background color and elevation, used when absolutely necessary e.g. button requires visual separation from a patterned background. @supported Available in v5.x with theme version 3
- * - `contained-tonal` - button with a secondary background color, an alternative middle ground between contained and outlined buttons. @supported Available in v5.x with theme version 3
+ * - `text` - flat button without background or outline, used for the lowest priority actions, especially when presenting multiple options.
*/
- mode?: 'text' | 'outlined' | 'contained' | 'elevated' | 'contained-tonal';
+ mode?: 'text' | 'outlined' | 'filled' | 'elevated' | 'tonal';
/**
- * Whether the color is a dark color. A dark button will render light text and vice-versa. Only applicable for:
- * * `contained` mode for theme version 2
- * * `contained`, `contained-tonal` and `elevated` modes for theme version 3.
+ * Whether the color is a dark color. A dark button will render light text and vice-versa. Only applicable for the `filled`, `tonal` and `elevated` modes.
*/
dark?: boolean;
/**
* Use a compact look, useful for `text` buttons in a row.
*/
compact?: boolean;
+ /**
+ * Size of the button (Material Design 3 expressive). One of
+ * `'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'`.
+ *
+ * When omitted, the button uses its legacy visuals. When set, the size
+ * controls the minimum height, horizontal padding, icon size, the gap
+ * between icon and label, and the label typescale.
+ */
+ size?: ButtonSize;
+ /**
+ * Shape variant of the button (Material Design 3 expressive). `'round'`
+ * uses the full-pill corner radius; `'square'` uses a smaller per-size
+ * corner radius. When omitted, the button keeps its legacy corner radius
+ * (`theme.shapes.corner.largeIncreased`). Overridden by an explicit
+ * `borderRadius` in `style`.
+ */
+ shape?: ButtonShape;
+ /**
+ * Whether this button is in the selected state (Material Design 3
+ * expressive toggle). When `true`:
+ *
+ * - The `shape` is flipped: `'round'` becomes `'square'` and vice versa.
+ * - For `outlined` and `text` modes, the button adopts a filled
+ * `secondaryContainer` appearance (matches `tonal`).
+ * - `accessibilityState.selected` is set so screen readers announce the
+ * toggle state.
+ *
+ * Other modes only flip the shape.
+ */
+ selected?: boolean;
/**
* Custom button's background color.
*/
@@ -67,6 +106,10 @@ export type Props = $Omit, 'mode'> & {
* Icon to display for the `Button`.
*/
icon?: IconSource;
+ /**
+ * Position of the `icon` relative to the label. Defaults to `'leading'`.
+ */
+ iconPosition?: 'leading' | 'trailing';
/**
* Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch.
*/
@@ -74,9 +117,14 @@ export type Props = $Omit, 'mode'> & {
/**
* Label text of the button.
*/
- children: React.ReactNode;
+ label?: string;
+ /**
+ * @deprecated Use `label` instead. When both `label` and `children` are set, `label` is used.
+ * Label text of the button.
+ */
+ children?: React.ReactNode;
/**
- * Make the label text uppercased. Note that this won't work if you pass React elements as children.
+ * Make the label text uppercased.
*/
uppercase?: boolean;
/**
@@ -84,6 +132,11 @@ export type Props = $Omit, 'mode'> & {
* https://reactnative.dev/docs/pressable#rippleconfig
*/
background?: PressableAndroidRippleConfig;
+ /**
+ * Color of the ripple effect / state layer. Defaults to the label color at
+ * the pressed-state opacity.
+ */
+ rippleColor?: ColorValue;
/**
* Accessibility label for the button. This is read by the screen reader when the user taps the button.
*/
@@ -118,7 +171,10 @@ export type Props = $Omit, 'mode'> & {
delayLongPress?: number;
/**
* Style of button's inner content.
- * Use this prop to apply custom height and width, to set a custom padding or to set the icon on the right with `flexDirection: 'row-reverse'`.
+ * Use this prop to apply custom height and width or to set a custom padding.
+ *
+ * Note: setting `flexDirection: 'row-reverse'` here to move the icon to the
+ * trailing edge is deprecated — use the `iconPosition` prop instead.
*/
contentStyle?: StyleProp;
/**
@@ -157,24 +213,43 @@ export type Props = $Omit, 'mode'> & {
* import { Button } from 'react-native-paper';
*
* const MyComponent = () => (
- * console.log('Pressed')}>
- * Press me
- *
+ * console.log('Pressed')}
+ * label="Press me"
+ * />
* );
*
* export default MyComponent;
* ```
*/
+
+// Elevation levels (MD3) used by the `elevated` mode: level 1 at rest,
+// level 2 while pressed.
+const initialElevation = 1;
+const activeElevation = 2;
+// MD3 leading/trailing icon size for the legacy (no-`size`) button.
+const iconSize = 20;
+// Minimum accessible touch target (dp). Extra-small/small buttons are shorter
+// than this and get expanded via hitSlop.
+const MIN_TOUCH_TARGET = 48;
+
const Button = (
{
disabled,
compact,
- mode = 'text',
+ mode = 'filled',
+ size,
+ shape,
+ selected,
dark,
loading,
icon,
+ iconPosition,
buttonColor: customButtonColor,
- textColor: customTextColor,
+ textColor: customLabelColor,
+ label,
children,
accessibilityLabel,
accessibilityHint,
@@ -193,6 +268,7 @@ const Button = (
testID = 'button',
accessible,
background,
+ rippleColor: customRippleColor,
maxFontSizeMultiplier,
touchableRef,
...rest
@@ -200,16 +276,40 @@ const Button = (
ref: React.ForwardedRef
) => {
const theme = useInternalTheme(themeOverrides);
- const isMode = React.useCallback(
- (modeToCompare: ButtonMode) => {
- return mode === modeToCompare;
- },
- [mode]
- );
+ const { direction } = useLocale();
+ const isMode = (modeToCompare: ButtonMode) => mode === modeToCompare;
const { animation } = theme;
const uppercase = uppercaseProp ?? false;
const isWeb = Platform.OS === 'web';
+ if (process.env.NODE_ENV !== 'production' && children != null) {
+ console.warn(
+ 'Button: the `children` prop is deprecated and will be removed in a future release. Use the `label` prop instead.'
+ );
+ }
+
+ const labelContent = label != null ? label : children;
+
+ const flattenedContentStyle = React.useMemo(
+ () => StyleSheet.flatten(contentStyle) as ViewStyle | undefined,
+ [contentStyle]
+ );
+ const usesReverseContentStyle =
+ flattenedContentStyle?.flexDirection === 'row-reverse';
+
+ if (process.env.NODE_ENV !== 'production' && usesReverseContentStyle) {
+ console.warn(
+ 'Button: setting `flexDirection: \'row-reverse\'` in `contentStyle` to move the icon to the trailing edge is deprecated. Use the `iconPosition="trailing"` prop instead.'
+ );
+ }
+
+ const requestedTrailingIcon =
+ iconPosition === 'trailing' || usesReverseContentStyle;
+ const shouldFlipForRTL = direction !== getDefaultDirection();
+ const isTrailingIcon = shouldFlipForRTL
+ ? !requestedTrailingIcon
+ : requestedTrailingIcon;
+
const hasPassedTouchHandler = hasTouchHandler({
onPress,
onPressIn,
@@ -218,8 +318,6 @@ const Button = (
});
const isElevationEntitled = !disabled && isMode('elevated');
- const initialElevation = 1;
- const activeElevation = 2;
const { current: elevation } = React.useRef(
new Animated.Value(isElevationEntitled ? initialElevation : 0)
@@ -233,95 +331,254 @@ const Button = (
duration: 0,
useNativeDriver: true,
});
- }, [isElevationEntitled, elevation, initialElevation]);
-
- 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,
+ }, [isElevationEntitled, elevation]);
+
+ const borderRadiusStyles = React.useMemo(() => {
+ const flattenedStyles = (StyleSheet.flatten(style) || {}) as ViewStyle;
+ const [, radiusStyles] = splitStyles(
+ flattenedStyles,
+ (key) => key.startsWith('border') && key.endsWith('Radius')
+ );
+ return radiusStyles;
+ }, [style]);
+
+ // When the button is `selected`, flip the requested shape so the
+ // unselected/selected pair contrasts visually (round ↔ square).
+ const effectiveShape: ButtonShape | undefined = shape
+ ? selected
+ ? shape === 'round'
+ ? 'square'
+ : 'round'
+ : shape
+ : undefined;
+ const staticRadius = effectiveShape
+ ? getButtonShapeRadius({ size, shape: effectiveShape, theme })
+ : theme.shapes.corner.largeIncreased;
+
+ const sizeStyle = React.useMemo(
+ () => (size ? getButtonSizeStyle(size) : undefined),
+ [size]
+ );
+
+ // Shape morph: animate the corner on press (→ corner.small) and on the
+ // `selected`/shape toggle. Shaped buttons only; skip a user-pinned radius.
+ const hasPinnedRadius = Object.keys(borderRadiusStyles).length > 0;
+ const animateShape = shape != null && !hasPinnedRadius;
+ // Use the real pill radius (minHeight/2) for `round` so the spring stays
+ // bounded instead of animating from the full-pill sentinel.
+ const restingRadius =
+ effectiveShape === 'round' && sizeStyle
+ ? sizeStyle.minHeight / 2
+ : staticRadius;
+ const pressedRadius = theme.shapes.corner.small;
+ const { current: animatedRadius } = React.useRef(
+ new Animated.Value(restingRadius)
+ );
+ const restingRadiusRef = React.useRef(restingRadius);
+ const isRadiusMountedRef = React.useRef(false);
+
+ const springRadiusTo = React.useCallback(
+ (toValue: number) => {
+ Animated.spring(animatedRadius, {
+ toValue,
+ ...toRawSpring(theme.motion.spring.fast.spatial),
+ useNativeDriver: false,
}).start();
+ },
+ [animatedRadius, theme]
+ );
+
+ const handlePressIn = React.useCallback(
+ (e: GestureResponderEvent) => {
+ onPressIn?.(e);
+ if (animateShape) {
+ springRadiusTo(pressedRadius);
+ }
+ if (mode === 'elevated') {
+ const { scale } = animation;
+ Animated.timing(elevation, {
+ toValue: activeElevation,
+ duration: 200 * scale,
+ useNativeDriver:
+ isWeb || Platform.constants.reactNativeVersion.minor <= 72,
+ }).start();
+ }
+ },
+ [
+ onPressIn,
+ animateShape,
+ springRadiusTo,
+ pressedRadius,
+ mode,
+ animation,
+ elevation,
+ isWeb,
+ ]
+ );
+
+ const handlePressOut = React.useCallback(
+ (e: GestureResponderEvent) => {
+ onPressOut?.(e);
+ if (animateShape) {
+ springRadiusTo(restingRadiusRef.current);
+ }
+ if (mode === 'elevated') {
+ const { scale } = animation;
+ Animated.timing(elevation, {
+ toValue: initialElevation,
+ duration: 150 * scale,
+ useNativeDriver:
+ isWeb || Platform.constants.reactNativeVersion.minor <= 72,
+ }).start();
+ }
+ },
+ [
+ onPressOut,
+ animateShape,
+ springRadiusTo,
+ mode,
+ animation,
+ elevation,
+ isWeb,
+ ]
+ );
+
+ // Snap on mount; animate when a toggle/shape change moves the resting radius.
+ React.useEffect(() => {
+ restingRadiusRef.current = restingRadius;
+ if (!isRadiusMountedRef.current) {
+ isRadiusMountedRef.current = true;
+ return;
}
- };
-
- const handlePressOut = (e: GestureResponderEvent) => {
- onPressOut?.(e);
- if (isMode('elevated')) {
- const { scale } = animation;
- Animated.timing(elevation, {
- toValue: initialElevation,
- duration: 150 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
+ if (animateShape) {
+ springRadiusTo(restingRadius);
+ } else {
+ animatedRadius.setValue(restingRadius);
}
- };
+ }, [restingRadius, animateShape, animatedRadius, springRadiusTo]);
- const flattenedStyles = (StyleSheet.flatten(style) || {}) as ViewStyle;
- const [, borderRadiusStyles] = splitStyles(
- flattenedStyles,
- (style) => style.startsWith('border') && style.endsWith('Radius')
+ // Clamp so a spring overshoot can never render a negative radius.
+ const surfaceRadius = React.useMemo(
+ () =>
+ animateShape
+ ? animatedRadius.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ extrapolateLeft: 'clamp',
+ })
+ : staticRadius,
+ [animateShape, animatedRadius, staticRadius]
);
- const borderRadius = theme.shapes.corner.largeIncreased;
- const iconSize = 18;
-
const {
backgroundColor,
borderColor,
- textColor,
- textOpacity,
+ labelColor,
+ labelOpacity,
borderWidth,
backgroundOpacity,
- } = getButtonColors({
- customButtonColor,
- customTextColor,
- theme,
- mode,
- disabled,
- dark,
- });
+ } = React.useMemo(
+ () =>
+ getButtonColors({
+ customButtonColor,
+ customLabelColor,
+ theme,
+ mode,
+ disabled,
+ dark,
+ selected,
+ }),
+ [customButtonColor, customLabelColor, theme, mode, disabled, dark, selected]
+ );
- const touchableStyle = {
- ...borderRadiusStyles,
- borderRadius: borderRadiusStyles.borderRadius ?? borderRadius,
- };
+ const rippleColor = React.useMemo(
+ () => getButtonRippleColor({ labelColor, customRippleColor }),
+ [labelColor, customRippleColor]
+ );
- const buttonStyle = {
- backgroundColor: backgroundOpacity < 1 ? 'transparent' : backgroundColor,
- borderColor,
- borderWidth,
- ...touchableStyle,
- };
-
- const { color: customLabelColor, fontSize: customLabelSize } =
- StyleSheet.flatten(labelStyle) || {};
-
- const font = (theme as Theme).fonts.labelLarge;
-
- const textStyle = {
- color: textColor,
- ...font,
- };
-
- const iconStyle =
- StyleSheet.flatten(contentStyle)?.flexDirection === 'row-reverse'
- ? [
- styles.iconReverse,
- styles[`md3IconReverse${compact ? 'Compact' : ''}`],
- isMode('text') &&
- styles[`md3IconReverseTextMode${compact ? 'Compact' : ''}`],
- ]
- : [
- styles.icon,
- styles[`md3Icon${compact ? 'Compact' : ''}`],
- isMode('text') &&
- styles[`md3IconTextMode${compact ? 'Compact' : ''}`],
- ];
+ const touchableStyle = React.useMemo(
+ () => ({
+ ...borderRadiusStyles,
+ borderRadius: borderRadiusStyles.borderRadius ?? staticRadius,
+ }),
+ [borderRadiusStyles, staticRadius]
+ );
+
+ const buttonStyle = React.useMemo(
+ () => ({
+ backgroundColor: backgroundOpacity < 1 ? 'transparent' : backgroundColor,
+ borderColor,
+ borderWidth,
+ ...touchableStyle,
+ borderRadius: surfaceRadius, // animated; ripple clip + overlay stay static
+ }),
+ [
+ backgroundOpacity,
+ backgroundColor,
+ borderColor,
+ borderWidth,
+ touchableStyle,
+ surfaceRadius,
+ ]
+ );
+
+ const touchableRippleStyle = React.useMemo(
+ () =>
+ // Web can't animate the inner radius — use a rect clipped by the Surface.
+ isWeb && animateShape
+ ? { borderRadius: 0 }
+ : getButtonTouchableRippleStyle(touchableStyle, borderWidth),
+ [isWeb, animateShape, touchableStyle, borderWidth]
+ );
+
+ const { color: labelStyleColor, fontSize: labelStyleSize } = React.useMemo(
+ () => StyleSheet.flatten(labelStyle) || {},
+ [labelStyle]
+ );
+
+ // Extra-small/small buttons are shorter than the 48dp minimum accessible
+ // touch target, so expand the press area with hitSlop without changing the
+ // visual size. A user-supplied `hitSlop` wins on the axes it sets.
+ const hitSlopWithMinTarget = React.useMemo(() => {
+ const verticalSlop = sizeStyle
+ ? Math.max(0, (MIN_TOUCH_TARGET - sizeStyle.minHeight) / 2)
+ : 0;
+ if (verticalSlop === 0) {
+ return hitSlop;
+ }
+ if (hitSlop == null) {
+ return { top: verticalSlop, bottom: verticalSlop };
+ }
+ // A numeric hitSlop is an explicit uniform override — respect it as-is.
+ if (typeof hitSlop === 'number') {
+ return hitSlop;
+ }
+ return {
+ ...hitSlop,
+ top: hitSlop.top ?? verticalSlop,
+ bottom: hitSlop.bottom ?? verticalSlop,
+ };
+ }, [hitSlop, sizeStyle]);
+
+ const labelTypeStyle = React.useMemo(
+ () => ({
+ color: labelColor,
+ ...(theme as Theme).fonts[sizeStyle?.labelVariant ?? 'labelLarge'],
+ }),
+ [labelColor, theme, sizeStyle]
+ );
+
+ const iconStyle = React.useMemo(
+ () =>
+ sizeStyle
+ ? null
+ : getButtonIconStyle({
+ mode,
+ compact,
+ position: isTrailingIcon ? 'trailing' : 'leading',
+ }),
+ [mode, compact, isTrailingIcon, sizeStyle]
+ );
return (
>
}
@@ -355,6 +614,7 @@ const Button = (
-
+
{icon && loading !== true ? (
-
+
) : null}
{loading ? (
) : null}
- {children}
+ {labelContent}
@@ -429,6 +710,9 @@ const styles = StyleSheet.create({
minWidth: 64,
borderStyle: 'solid',
},
+ clip: {
+ overflow: 'hidden',
+ },
compact: {
minWidth: 'auto',
},
@@ -437,67 +721,32 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
- icon: {
- marginLeft: 12,
- marginRight: -4,
- },
- iconReverse: {
- marginRight: 12,
- marginLeft: -4,
+ contentReverse: {
+ flexDirection: 'row-reverse',
},
- /* eslint-disable react-native/no-unused-styles */
- md3Icon: {
- marginLeft: 16,
- marginRight: -16,
- },
- md3IconCompact: {
- marginLeft: 8,
- marginRight: 0,
- },
- md3IconReverse: {
- marginLeft: -16,
- marginRight: 16,
- },
- md3IconReverseCompact: {
- marginLeft: 0,
- marginRight: 8,
- },
- md3IconTextMode: {
- marginLeft: 12,
- marginRight: -8,
- },
- md3IconTextModeCompact: {
- marginLeft: 6,
- marginRight: 0,
- },
- md3IconReverseTextMode: {
- marginLeft: -8,
- marginRight: 12,
- },
- md3IconReverseTextModeCompact: {
- marginLeft: 0,
- marginRight: 6,
- },
- /* eslint-enable react-native/no-unused-styles */
label: {
textAlign: 'center',
marginVertical: 9,
marginHorizontal: 16,
},
+ sizedLabel: {
+ marginVertical: 0,
+ marginHorizontal: 0,
+ },
compactLabel: {
marginHorizontal: 8,
},
uppercaseLabel: {
textTransform: 'uppercase',
},
- md3Label: {
+ legacyLabel: {
marginVertical: 10,
- marginHorizontal: 24,
+ marginHorizontal: 16,
},
- md3LabelText: {
+ legacyLabelText: {
marginHorizontal: 12,
},
- md3LabelTextAddons: {
+ legacyLabelTextAddons: {
marginHorizontal: 16,
},
});
diff --git a/src/components/Button/tokens.ts b/src/components/Button/tokens.ts
new file mode 100644
index 0000000000..5efd7ca8d8
--- /dev/null
+++ b/src/components/Button/tokens.ts
@@ -0,0 +1,115 @@
+import type { ButtonLabelVariant, ButtonSize } from './utils';
+
+/**
+ * Shape keys for the button container corners. `'full'` resolves to the
+ * full-pill radius (`cornerFull`); every other key resolves against
+ * `theme.shapes.corner[key]`. Mirrors MD3 `ShapeKeyTokens`.
+ */
+export type ButtonCornerKey =
+ | 'full'
+ | 'small'
+ | 'medium'
+ | 'large'
+ | 'extraLarge';
+
+/**
+ * Per-size component tokens for the Material Design 3 (expressive) button
+ * sizes, modelled on Jetpack Compose's `Button{Size}Tokens`. Centralising
+ * these here keeps every size-specific metric in one place and references the
+ * theme shape tokens instead of magic numbers for the corner radii.
+ */
+export type ButtonSizeTokens = {
+ containerHeight: number;
+ iconSize: number;
+ iconLabelSpace: number;
+ leadingSpace: number;
+ trailingSpace: number;
+ outlinedOutlineWidth: number;
+ labelVariant: ButtonLabelVariant;
+ containerShapeRound: ButtonCornerKey;
+ containerShapeSquare: ButtonCornerKey;
+ /** Corner the container morphs to while pressed (MD3: always `small`). */
+ pressedContainerShape: ButtonCornerKey;
+ selectedContainerShapeRound: ButtonCornerKey;
+ selectedContainerShapeSquare: ButtonCornerKey;
+};
+
+export const buttonSizeTokens: Record = {
+ 'extra-small': {
+ containerHeight: 32,
+ iconSize: 20,
+ iconLabelSpace: 4,
+ leadingSpace: 12,
+ trailingSpace: 12,
+ outlinedOutlineWidth: 1,
+ labelVariant: 'labelLarge',
+ containerShapeRound: 'full',
+ containerShapeSquare: 'medium',
+ pressedContainerShape: 'small',
+ selectedContainerShapeRound: 'full',
+ selectedContainerShapeSquare: 'medium',
+ },
+ small: {
+ containerHeight: 40,
+ iconSize: 20,
+ iconLabelSpace: 8,
+ leadingSpace: 16,
+ trailingSpace: 16,
+ outlinedOutlineWidth: 1,
+ labelVariant: 'labelLarge',
+ containerShapeRound: 'full',
+ containerShapeSquare: 'medium',
+ pressedContainerShape: 'small',
+ selectedContainerShapeRound: 'full',
+ selectedContainerShapeSquare: 'medium',
+ },
+ medium: {
+ containerHeight: 56,
+ iconSize: 24,
+ iconLabelSpace: 8,
+ leadingSpace: 24,
+ trailingSpace: 24,
+ outlinedOutlineWidth: 1,
+ labelVariant: 'titleMedium',
+ containerShapeRound: 'full',
+ containerShapeSquare: 'large',
+ pressedContainerShape: 'small',
+ selectedContainerShapeRound: 'full',
+ selectedContainerShapeSquare: 'large',
+ },
+ large: {
+ containerHeight: 96,
+ iconSize: 32,
+ iconLabelSpace: 12,
+ leadingSpace: 48,
+ trailingSpace: 48,
+ outlinedOutlineWidth: 1,
+ labelVariant: 'headlineSmall',
+ containerShapeRound: 'full',
+ containerShapeSquare: 'extraLarge',
+ pressedContainerShape: 'small',
+ selectedContainerShapeRound: 'full',
+ selectedContainerShapeSquare: 'extraLarge',
+ },
+ 'extra-large': {
+ containerHeight: 136,
+ iconSize: 40,
+ iconLabelSpace: 16,
+ leadingSpace: 64,
+ trailingSpace: 64,
+ outlinedOutlineWidth: 1,
+ labelVariant: 'headlineLarge',
+ containerShapeRound: 'full',
+ containerShapeSquare: 'extraLarge',
+ pressedContainerShape: 'small',
+ selectedContainerShapeRound: 'full',
+ selectedContainerShapeSquare: 'extraLarge',
+ },
+};
+
+/** Corner used by the legacy (no-`size`) button for each shape variant. */
+export const legacyContainerShape: Record<'round' | 'square', ButtonCornerKey> =
+ {
+ round: 'full',
+ square: 'medium',
+ };
diff --git a/src/components/Button/utils.tsx b/src/components/Button/utils.tsx
index 03ce9513ea..46f9704580 100644
--- a/src/components/Button/utils.tsx
+++ b/src/components/Button/utils.tsx
@@ -1,23 +1,134 @@
import type { ColorValue, ViewStyle } from 'react-native';
+import color from 'color';
+
+import {
+ buttonSizeTokens,
+ legacyContainerShape,
+ type ButtonCornerKey,
+} from './tokens';
import { black, white } from '../../theme/colors';
import { tokens } from '../../theme/tokens';
+import { cornerFull } from '../../theme/tokens/sys/shape';
import type { InternalTheme, Theme } from '../../types';
import { splitStyles } from '../../utils/splitStyles';
const { stateOpacity } = tokens.md.ref;
-export type ButtonMode =
- | 'text'
- | 'outlined'
- | 'contained'
- | 'elevated'
- | 'contained-tonal';
+export type ButtonMode = 'text' | 'outlined' | 'filled' | 'elevated' | 'tonal';
+
+export type ButtonIconPosition = 'leading' | 'trailing';
+
+export type ButtonSize =
+ | 'extra-small'
+ | 'small'
+ | 'medium'
+ | 'large'
+ | 'extra-large';
+
+export type ButtonLabelVariant =
+ | 'labelLarge'
+ | 'titleMedium'
+ | 'headlineSmall'
+ | 'headlineLarge';
+
+export type ButtonSizeStyle = {
+ minHeight: number;
+ paddingHorizontal: number;
+ iconSize: number;
+ iconGap: number;
+ labelVariant: ButtonLabelVariant;
+};
+
+/**
+ * Per-size metrics for the Material Design 3 (expressive) button sizes, read
+ * from the component tokens. Used when the `size` prop is explicitly set; if
+ * `size` is omitted, the Button keeps its legacy visuals.
+ */
+export const getButtonSizeStyle = (size: ButtonSize): ButtonSizeStyle => {
+ const t = buttonSizeTokens[size];
+ return {
+ minHeight: t.containerHeight,
+ paddingHorizontal: t.leadingSpace,
+ iconSize: t.iconSize,
+ iconGap: t.iconLabelSpace,
+ labelVariant: t.labelVariant,
+ };
+};
+
+export type ButtonShape = 'round' | 'square';
+
+/**
+ * Resolves a shape key against the theme: `'full'` is the full-pill radius;
+ * every other key maps to `theme.shapes.corner[key]`.
+ */
+export const resolveButtonCorner = (
+ theme: InternalTheme,
+ key: ButtonCornerKey
+): number =>
+ key === 'full' ? cornerFull : (theme as Theme).shapes.corner[key];
+
+/**
+ * Corner radius for the requested shape, read from the component tokens and
+ * resolved against the theme shape tokens. `round` is the full-pill radius;
+ * `square` uses a per-size smaller corner. When `size` is omitted the legacy
+ * shape mapping is used.
+ */
+export const getButtonShapeRadius = ({
+ size,
+ shape,
+ theme,
+}: {
+ size?: ButtonSize;
+ shape: ButtonShape;
+ theme: InternalTheme;
+}): number => {
+ const key = size
+ ? shape === 'round'
+ ? buttonSizeTokens[size].containerShapeRound
+ : buttonSizeTokens[size].containerShapeSquare
+ : legacyContainerShape[shape];
+ return resolveButtonCorner(theme, key);
+};
+
+/**
+ * Returns the margins applied to the button's icon (or loading indicator)
+ * depending on the button mode, density and the position of the icon relative
+ * to the label.
+ */
+export const getButtonIconStyle = ({
+ mode,
+ compact,
+ position,
+}: {
+ mode: ButtonMode;
+ compact?: boolean;
+ position: ButtonIconPosition;
+}): Pick => {
+ const isTextMode = mode === 'text';
+
+ if (position === 'trailing') {
+ if (compact) {
+ return { marginLeft: 0, marginRight: isTextMode ? 6 : 8 };
+ }
+ return isTextMode
+ ? { marginLeft: -8, marginRight: 12 }
+ : { marginLeft: -8, marginRight: 16 };
+ }
+
+ if (compact) {
+ return { marginLeft: isTextMode ? 6 : 8, marginRight: 0 };
+ }
+ return isTextMode
+ ? { marginLeft: 12, marginRight: -8 }
+ : { marginLeft: 16, marginRight: -8 };
+};
type BaseProps = {
isMode: (mode: ButtonMode) => boolean;
theme: InternalTheme;
disabled?: boolean;
+ selected?: boolean;
};
const isDark = ({
@@ -43,6 +154,7 @@ const getButtonBackgroundColor = ({
theme,
disabled,
customButtonColor,
+ selected,
}: BaseProps & {
customButtonColor?: ColorValue;
}) => {
@@ -58,76 +170,101 @@ const getButtonBackgroundColor = ({
return colors.onSurface;
}
+ // Selected toggle (only outlined/text adopt a filled "tonal-selected" look;
+ // filled / tonal / elevated already render filled).
+ if (selected && (isMode('outlined') || isMode('text'))) {
+ return colors.secondaryContainer;
+ }
+
if (isMode('elevated')) {
return colors.surfaceContainerLow;
}
- if (isMode('contained')) {
+ if (isMode('filled')) {
return colors.primary;
}
- if (isMode('contained-tonal')) {
+ if (isMode('tonal')) {
return colors.secondaryContainer;
}
return 'transparent';
};
-const getButtonTextColor = ({
+const getButtonLabelColor = ({
isMode,
theme,
disabled,
- customTextColor,
+ customLabelColor,
backgroundColor,
dark,
+ selected,
}: BaseProps & {
- customTextColor?: ColorValue;
+ customLabelColor?: ColorValue;
backgroundColor: ColorValue;
dark?: boolean;
}) => {
const { colors } = theme as Theme;
- if (customTextColor && !disabled) {
- return customTextColor;
+ if (customLabelColor && !disabled) {
+ return customLabelColor;
}
if (disabled) {
return theme.colors.onSurface;
}
+ // Selected toggle for outlined/text mirrors the tonal label color.
+ if (selected && (isMode('outlined') || isMode('text'))) {
+ return colors.onSecondaryContainer;
+ }
+
if (typeof dark === 'boolean') {
- if (
- isMode('contained') ||
- isMode('contained-tonal') ||
- isMode('elevated')
- ) {
+ if (isMode('filled') || isMode('tonal') || isMode('elevated')) {
return isDark({ dark, backgroundColor }) ? white : black;
}
}
- if (isMode('outlined') || isMode('text') || isMode('elevated')) {
+ // Outlined uses the neutral on-surface-variant label per MD3 spec; text and
+ // elevated keep the primary accent.
+ if (isMode('outlined')) {
+ return colors.onSurfaceVariant;
+ }
+
+ if (isMode('text') || isMode('elevated')) {
return colors.primary;
}
- if (isMode('contained')) {
+ if (isMode('filled')) {
return colors.onPrimary;
}
- if (isMode('contained-tonal')) {
+ if (isMode('tonal')) {
return colors.onSecondaryContainer;
}
return colors.primary;
};
-const getButtonBorderColor = ({ isMode, theme }: BaseProps) => {
+const getButtonBorderColor = ({ isMode, theme, selected }: BaseProps) => {
+ // A selected outlined toggle drops its outline (the filled background takes
+ // over as the visual affordance).
+ if (selected && isMode('outlined')) {
+ return 'transparent';
+ }
if (isMode('outlined')) {
- return theme.colors.outlineVariant;
+ return theme.colors.outline;
}
return 'transparent';
};
-const getButtonBorderWidth = ({ isMode }: Omit) => {
+const getButtonBorderWidth = ({
+ isMode,
+ selected,
+}: Omit) => {
+ if (selected && isMode('outlined')) {
+ return 0;
+ }
if (isMode('outlined')) {
return 1;
}
@@ -139,16 +276,18 @@ export const getButtonColors = ({
theme,
mode,
customButtonColor,
- customTextColor,
+ customLabelColor,
disabled,
dark,
+ selected,
}: {
theme: InternalTheme;
mode: ButtonMode;
customButtonColor?: ColorValue;
- customTextColor?: ColorValue;
+ customLabelColor?: ColorValue;
disabled?: boolean;
dark?: boolean;
+ selected?: boolean;
}) => {
const isMode = (modeToCompare: ButtonMode) => {
return mode === modeToCompare;
@@ -159,22 +298,24 @@ export const getButtonColors = ({
theme,
disabled,
customButtonColor,
+ selected,
});
- const textColor = getButtonTextColor({
+ const labelColor = getButtonLabelColor({
isMode,
theme,
disabled,
- customTextColor,
+ customLabelColor,
backgroundColor,
dark,
+ selected,
});
- const borderColor = getButtonBorderColor({ isMode, theme });
+ const borderColor = getButtonBorderColor({ isMode, theme, selected });
- const borderWidth = getButtonBorderWidth({ isMode, theme });
+ const borderWidth = getButtonBorderWidth({ isMode, selected });
- const textOpacity = disabled ? stateOpacity.disabled : stateOpacity.enabled;
+ const labelOpacity = disabled ? stateOpacity.disabled : stateOpacity.enabled;
const backgroundOpacity =
disabled && !isMode('outlined') && !isMode('text')
@@ -184,13 +325,40 @@ export const getButtonColors = ({
return {
backgroundColor,
borderColor,
- textColor,
- textOpacity,
+ labelColor,
+ labelOpacity,
borderWidth,
backgroundOpacity,
};
};
+/**
+ * Returns the color used for the button's ripple / state layer. Defaults to
+ * the label color at the pressed-state opacity (per Material Design 3), unless
+ * a custom ripple color is provided.
+ *
+ * When the label color is not a plain string (e.g. an Android Material You
+ * `PlatformColor`), `undefined` is returned so `TouchableRipple` falls back to
+ * its own default state-layer color.
+ */
+export const getButtonRippleColor = ({
+ labelColor,
+ customRippleColor,
+}: {
+ labelColor: ColorValue;
+ customRippleColor?: ColorValue;
+}): ColorValue | undefined => {
+ if (customRippleColor) {
+ return customRippleColor;
+ }
+
+ if (typeof labelColor !== 'string') {
+ return undefined;
+ }
+
+ return color(labelColor).alpha(stateOpacity.pressed).rgb().string();
+};
+
type ViewStyleBorderRadiusStyles = Partial<
Pick<
ViewStyle,
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
index 6dc433322a..1e1677c5d2 100644
--- a/src/components/Card/Card.tsx
+++ b/src/components/Card/Card.tsx
@@ -127,8 +127,8 @@ export type Props = $Omit, 'mode'> & {
*
*
*
- * Cancel
- * Ok
+ *
+ *
*
*
* );
diff --git a/src/components/Card/CardActions.tsx b/src/components/Card/CardActions.tsx
index daa26a19fd..a795619483 100644
--- a/src/components/Card/CardActions.tsx
+++ b/src/components/Card/CardActions.tsx
@@ -25,8 +25,8 @@ export type Props = React.ComponentPropsWithRef & {
* const MyComponent = () => (
*
*
- * Cancel
- * Ok
+ *
+ *
*
*
* );
@@ -48,8 +48,7 @@ const CardActions = ({ theme, style, children, ...rest }: Props) => {
}
const compact = child.props.compact;
- const mode =
- child.props.mode ?? (index === 0 ? 'outlined' : 'contained');
+ const mode = child.props.mode ?? (index === 0 ? 'outlined' : 'filled');
const childStyle = [styles.button, child.props.style];
return React.cloneElement(child, {
diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx
index b6cfd9a64b..a035e951fa 100644
--- a/src/components/Checkbox/Checkbox.tsx
+++ b/src/components/Checkbox/Checkbox.tsx
@@ -27,6 +27,13 @@ export type Props = {
* Custom color for checkbox.
*/
color?: string;
+ /**
+ * Whether the checkbox is in an error state. When true, the outline
+ * (unchecked) and container (checked / indeterminate) use
+ * `theme.colors.error`. `disabled` and explicit `color`/`uncheckedColor`
+ * overrides take precedence.
+ */
+ error?: boolean;
/**
* @optional
*/
diff --git a/src/components/Checkbox/CheckboxAndroid.tsx b/src/components/Checkbox/CheckboxAndroid.tsx
index 8a93cc6a70..d73bdf57c5 100644
--- a/src/components/Checkbox/CheckboxAndroid.tsx
+++ b/src/components/Checkbox/CheckboxAndroid.tsx
@@ -34,6 +34,13 @@ export type Props = $RemoveChildren & {
* Custom color for checkbox.
*/
color?: ColorValue;
+ /**
+ * Whether the checkbox is in an error state. When true, the outline
+ * (unchecked) and container (checked / indeterminate) use
+ * `theme.colors.error`. `disabled` and explicit `color`/`uncheckedColor`
+ * overrides take precedence.
+ */
+ error?: boolean;
/**
* @optional
*/
@@ -60,6 +67,7 @@ const CheckboxAndroid = ({
disabled,
onPress,
testID,
+ error,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
@@ -107,6 +115,7 @@ const CheckboxAndroid = ({
checked,
customColor: rest.color,
customUncheckedColor: rest.uncheckedColor,
+ error,
});
const borderWidth = scaleAnim.interpolate({
diff --git a/src/components/Checkbox/CheckboxIOS.tsx b/src/components/Checkbox/CheckboxIOS.tsx
index d4a8127180..5830359372 100644
--- a/src/components/Checkbox/CheckboxIOS.tsx
+++ b/src/components/Checkbox/CheckboxIOS.tsx
@@ -29,6 +29,12 @@ export type Props = $RemoveChildren & {
* Custom color for checkbox.
*/
color?: ColorValue;
+ /**
+ * Whether the checkbox is in an error state. When true, the checked /
+ * indeterminate icon uses `theme.colors.error`. `disabled` and explicit
+ * `color` overrides take precedence.
+ */
+ error?: boolean;
/**
* @optional
*/
@@ -52,6 +58,7 @@ const CheckboxIOS = ({
onPress,
theme: themeOverrides,
testID,
+ error,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
@@ -62,6 +69,7 @@ const CheckboxIOS = ({
theme,
disabled,
customColor: rest.color,
+ error,
});
const icon = indeterminate ? 'minus' : 'check';
diff --git a/src/components/Checkbox/utils.ts b/src/components/Checkbox/utils.ts
index f47457151b..87362df00c 100644
--- a/src/components/Checkbox/utils.ts
+++ b/src/components/Checkbox/utils.ts
@@ -8,28 +8,40 @@ const { stateOpacity } = tokens.md.ref;
const getAndroidCheckedColor = ({
theme,
customColor,
+ error,
}: {
theme: InternalTheme;
customColor?: ColorValue;
+ error?: boolean;
}) => {
if (customColor) {
return customColor;
}
+ if (error) {
+ return theme.colors.error;
+ }
+
return theme.colors.primary;
};
const getAndroidUncheckedColor = ({
theme,
customUncheckedColor,
+ error,
}: {
theme: InternalTheme;
customUncheckedColor?: ColorValue;
+ error?: boolean;
}) => {
if (customUncheckedColor) {
return customUncheckedColor;
}
+ if (error) {
+ return theme.colors.error;
+ }
+
return theme.colors.onSurfaceVariant;
};
@@ -62,17 +74,20 @@ export const getAndroidSelectionControlColor = ({
checked,
customColor,
customUncheckedColor,
+ error,
}: {
theme: InternalTheme;
checked: boolean;
disabled?: boolean;
customColor?: ColorValue;
customUncheckedColor?: ColorValue;
+ error?: boolean;
}) => {
- const checkedColor = getAndroidCheckedColor({ theme, customColor });
+ const checkedColor = getAndroidCheckedColor({ theme, customColor, error });
const uncheckedColor = getAndroidUncheckedColor({
theme,
customUncheckedColor,
+ error,
});
const selectionControlOpacity = disabled
? stateOpacity.disabled
@@ -94,10 +109,12 @@ const getIOSCheckedColor = ({
theme,
disabled,
customColor,
+ error,
}: {
theme: InternalTheme;
customColor?: ColorValue;
disabled?: boolean;
+ error?: boolean;
}) => {
if (disabled) {
return theme.colors.primary;
@@ -107,6 +124,10 @@ const getIOSCheckedColor = ({
return customColor;
}
+ if (error) {
+ return theme.colors.error;
+ }
+
return theme.colors.primary;
};
@@ -114,12 +135,19 @@ export const getSelectionControlIOSColor = ({
theme,
disabled,
customColor,
+ error,
}: {
theme: InternalTheme;
disabled?: boolean;
customColor?: ColorValue;
+ error?: boolean;
}) => {
- const checkedColor = getIOSCheckedColor({ theme, disabled, customColor });
+ const checkedColor = getIOSCheckedColor({
+ theme,
+ disabled,
+ customColor,
+ error,
+ });
const checkedColorOpacity = disabled
? stateOpacity.disabled
: stateOpacity.enabled;
diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx
index 5e26051819..890202f75e 100644
--- a/src/components/DataTable/DataTablePagination.tsx
+++ b/src/components/DataTable/DataTablePagination.tsx
@@ -181,11 +181,10 @@ const PaginationDropdown = ({
onPress={() => toggleSelect(true)}
style={styles.button}
icon="menu-down"
- contentStyle={styles.contentStyle}
+ iconPosition="trailing"
theme={theme}
- >
- {`${numberOfItemsPerPage}`}
-
+ label={`${numberOfItemsPerPage}`}
+ />
}
>
{numberOfItemsPerPageList?.map((option) => (
@@ -359,9 +358,6 @@ const styles = StyleSheet.create({
iconsContainer: {
flexDirection: 'row',
},
- contentStyle: {
- flexDirection: 'row-reverse',
- },
});
export default DataTablePagination;
diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx
index 717445aed7..e2f9ec2491 100644
--- a/src/components/Dialog/Dialog.tsx
+++ b/src/components/Dialog/Dialog.tsx
@@ -73,7 +73,7 @@ const DIALOG_ELEVATION: number = 24;
* return (
*
*
- * Show Dialog
+ *
*
*
* Alert
@@ -81,7 +81,7 @@ const DIALOG_ELEVATION: number = 24;
* This is simple dialog
*
*
- * Done
+ *
*
*
*
diff --git a/src/components/Dialog/DialogActions.tsx b/src/components/Dialog/DialogActions.tsx
index 0c8d8c5f74..b3b532416d 100644
--- a/src/components/Dialog/DialogActions.tsx
+++ b/src/components/Dialog/DialogActions.tsx
@@ -34,8 +34,8 @@ export type Props = React.ComponentPropsWithRef & {
*
*
*
- * console.log('Cancel')}>Cancel
- * console.log('Ok')}>Ok
+ * console.log('Cancel')} label="Cancel" />
+ * console.log('Ok')} label="Ok" />
*
*
*
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
index 832eac30bb..5ff69f48ca 100644
--- a/src/components/Menu/Menu.tsx
+++ b/src/components/Menu/Menu.tsx
@@ -154,7 +154,7 @@ const isBrowser = () => Platform.OS === 'web' && 'document' in global;
* Show menu }>
+ * anchor={ }>
* {}} title="Item 1" />
* {}} title="Item 2" />
*
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
index 7cce27eb3c..c9cc80d42e 100644
--- a/src/components/Modal.tsx
+++ b/src/components/Modal.tsx
@@ -93,9 +93,7 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
* Example Modal. Click outside this area to dismiss.
*
*
- *
- * Show
- *
+ *
*
* );
* };
diff --git a/src/components/Snackbar.tsx b/src/components/Snackbar.tsx
index f6eb590e84..30d553114c 100644
--- a/src/components/Snackbar.tsx
+++ b/src/components/Snackbar.tsx
@@ -113,7 +113,7 @@ const DURATION_LONG = 10000;
*
* return (
*
- * {visible ? 'Hide' : 'Show'}
+ *
*
- {actionLabel}
-
+ />
) : null}
{isIconButton ? (
{
- const tree = render(Text Button ).toJSON();
+it('renders filled button by default', () => {
+ const tree = render( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders text button with mode', () => {
- const tree = render(Text Button ).toJSON();
+ const tree = render( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders outlined button with mode', () => {
const tree = render(
- Outlined Button
+
).toJSON();
expect(tree).toMatchSnapshot();
});
-it('renders contained contained with mode', () => {
+it('renders filled button with mode', () => {
const tree = render(
- Contained Button
+
).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders button with icon', () => {
- const tree = render(Icon Button ).toJSON();
+ const tree = render( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders button with icon in reverse order', () => {
const tree = render(
-
- Right Icon
-
+
).toJSON();
expect(tree).toMatchSnapshot();
});
+it('swaps the icon to the trailing edge under RTL', () => {
+ const { getByTestId: getByTestIdLTR } = render(
+
+ );
+ const { getByTestId: getByTestIdRTL } = render(
+
+
+
+ );
+
+ const ltrIconStyle = StyleSheet.flatten(
+ getByTestIdLTR('button-icon-container').props.style
+ );
+ const rtlIconStyle = StyleSheet.flatten(
+ getByTestIdRTL('button-icon-container').props.style
+ );
+
+ // The physical margins swap so a "leading" icon sits on the right in RTL.
+ expect(rtlIconStyle.marginLeft).toBe(ltrIconStyle.marginRight);
+ expect(rtlIconStyle.marginRight).toBe(ltrIconStyle.marginLeft);
+});
+
it('renders loading button', () => {
- const tree = render(Loading Button ).toJSON();
+ const tree = render( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders disabled button', () => {
- const tree = render(Disabled Button ).toJSON();
+ const tree = render( ).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders disabled button if there is no touch handler passed', () => {
const { getByTestId } = render(
- Disabled button
+
);
expect(getByTestId('disabled-button').props.accessibilityState).toMatchObject(
@@ -97,9 +131,11 @@ it('renders disabled button if there is no touch handler passed', () => {
it('renders active button if only onLongPress handler is passed', () => {
const { getByTestId } = render(
- {}} testID="active-button">
- Active button
-
+ {}}
+ testID="active-button"
+ label="Active button"
+ />
);
expect(getByTestId('active-button').props.accessibilityState).toMatchObject({
@@ -109,7 +145,7 @@ it('renders active button if only onLongPress handler is passed', () => {
it('renders button with color', () => {
const tree = render(
- Custom Button
+
).toJSON();
expect(tree).toMatchSnapshot();
@@ -117,7 +153,7 @@ it('renders button with color', () => {
it('renders button with button color', () => {
const tree = render(
- Custom Button
+
).toJSON();
expect(tree).toMatchSnapshot();
@@ -125,7 +161,7 @@ it('renders button with button color', () => {
it('renders button with custom testID', () => {
const tree = render(
- Button with custom testID
+
).toJSON();
expect(tree).toMatchSnapshot();
@@ -133,9 +169,10 @@ it('renders button with custom testID', () => {
it('renders button with an accessibility label', () => {
const tree = render(
-
- Button with accessibility label
-
+
).toJSON();
expect(tree).toMatchSnapshot();
@@ -143,7 +180,7 @@ it('renders button with an accessibility label', () => {
it('renders button with an accessibility hint', () => {
const tree = render(
- Button with accessibility hint
+
).toJSON();
expect(tree).toMatchSnapshot();
@@ -151,9 +188,11 @@ it('renders button with an accessibility hint', () => {
it('renders button with custom border radius', () => {
const { getByTestId } = render(
-
- Custom radius
-
+
);
expect(getByTestId('custom-radius-container')).toHaveStyle(
@@ -168,9 +207,8 @@ it('renders outlined button with custom border radius', () => {
mode={'outlined'}
testID="custom-radius"
style={styles.customRadius}
- >
- Custom radius
-
+ label="Custom radius"
+ />
);
expect(getByTestId('custom-radius-container')).toHaveStyle(
@@ -186,9 +224,11 @@ it('renders outlined button with custom border radius', () => {
it('renders button without border radius', () => {
const { getByTestId } = render(
-
- Custom radius
-
+
);
expect(getByTestId('custom-radius-container')).toHaveStyle(styles.noRadius);
@@ -200,9 +240,7 @@ it('should execute onPressIn', () => {
const onPress = jest.fn();
const { getByTestId } = render(
-
- {null}
-
+
);
fireEvent(getByTestId('button'), 'onPressIn');
expect(onPressInMock).toHaveBeenCalledTimes(1);
@@ -213,20 +251,56 @@ it('should execute onPressOut', () => {
const onPress = jest.fn();
const { getByTestId } = render(
-
- {null}
-
+
);
fireEvent(getByTestId('button'), 'onPressOut');
expect(onPressOutMock).toHaveBeenCalledTimes(1);
});
+describe('label prop', () => {
+ it('renders the label text', () => {
+ const { getByTestId } = render( );
+
+ expect(getByTestId('button-text')).toHaveTextContent('My label');
+ });
+
+ it('takes precedence over children', () => {
+ const { getByTestId } = render(
+
+ From children
+
+ );
+
+ expect(getByTestId('button-text')).toHaveTextContent('From label');
+ });
+});
+
+describe('deprecated children prop', () => {
+ it('still renders the children as the label', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ const { getByTestId } = render(
+ Legacy label
+ );
+
+ expect(getByTestId('button-text')).toHaveTextContent('Legacy label');
+ warn.mockRestore();
+ });
+
+ it('warns about the deprecation', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ render(Legacy label );
+
+ expect(warn).toHaveBeenCalledWith(
+ expect.stringContaining('`children` prop is deprecated')
+ );
+ warn.mockRestore();
+ });
+});
+
describe('button text styles', () => {
it('applies uppercase styles if uppercase prop is truthy', () => {
const { getByTestId } = render(
-
- Test
-
+
);
expect(getByTestId('button-text')).toHaveStyle({
@@ -236,9 +310,7 @@ describe('button text styles', () => {
it('does not apply uppercase styles if uppercase prop is falsy', () => {
const { getByTestId } = render(
-
- Test
-
+
);
expect(getByTestId('button-text')).not.toHaveStyle({
@@ -250,9 +322,13 @@ describe('button text styles', () => {
describe('button icon styles', () => {
it('should return correct icon styles for compact text button', () => {
const { getByTestId } = render(
-
- Compact text button
-
+
);
expect(getByTestId('compact-button-icon-container')).toHaveStyle({
marginLeft: 6,
@@ -260,26 +336,32 @@ describe('button icon styles', () => {
});
});
- (['outlined', 'contained', 'contained-tonal', 'elevated'] as const).forEach(
- (mode) =>
- it(`should return correct icon styles for compact ${mode} button`, () => {
- const { getByTestId } = render(
-
- Compact {mode} button
-
- );
- expect(getByTestId('compact-button-icon-container')).toHaveStyle({
- marginLeft: 8,
- marginRight: 0,
- });
- })
+ (['outlined', 'filled', 'tonal', 'elevated'] as const).forEach((mode) =>
+ it(`should return correct icon styles for compact ${mode} button`, () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('compact-button-icon-container')).toHaveStyle({
+ marginLeft: 8,
+ marginRight: 0,
+ });
+ })
);
it('should return correct icon styles for text button', () => {
const { getByTestId } = render(
-
- text button
-
+
);
expect(getByTestId('compact-button-icon-container')).toHaveStyle({
marginLeft: 12,
@@ -287,22 +369,76 @@ describe('button icon styles', () => {
});
});
- (['outlined', 'contained', 'contained-tonal', 'elevated'] as const).forEach(
- (mode) =>
- it(`should return correct icon styles for compact ${mode} button`, () => {
- const { getByTestId } = render(
-
- {mode} button
-
- );
- expect(getByTestId('compact-button-icon-container')).toHaveStyle({
- marginLeft: 16,
- marginRight: -16,
- });
- })
+ (['outlined', 'filled', 'tonal', 'elevated'] as const).forEach((mode) =>
+ it(`should return correct icon styles for compact ${mode} button`, () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('compact-button-icon-container')).toHaveStyle({
+ marginLeft: 16,
+ marginRight: -8,
+ });
+ })
);
});
+describe('icon position', () => {
+ it('places the icon before the label by default', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-icon-container')).toHaveStyle({
+ marginLeft: 16,
+ marginRight: -8,
+ });
+ });
+
+ it('places the icon after the label when iconPosition is "trailing"', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-icon-container')).toHaveStyle({
+ marginLeft: -8,
+ marginRight: 16,
+ });
+ });
+
+ it('still flips the icon via the deprecated contentStyle row-reverse and warns', () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-icon-container')).toHaveStyle({
+ marginLeft: -8,
+ marginRight: 16,
+ });
+ expect(warn).toHaveBeenCalledWith(
+ expect.stringContaining('`contentStyle`')
+ );
+ warn.mockRestore();
+ });
+});
+
describe('getButtonColors - background color', () => {
const customButtonColor = '#111111';
@@ -343,7 +479,7 @@ describe('getButtonColors - background color', () => {
})
);
- (['contained', 'contained-tonal', 'elevated'] as const).forEach((mode) =>
+ (['filled', 'tonal', 'elevated'] as const).forEach((mode) =>
it(`should return correct disabled color, for theme version 3, ${mode} mode`, () => {
return expect(
getButtonColors({
@@ -359,7 +495,7 @@ describe('getButtonColors - background color', () => {
})
);
- (['contained', 'contained-tonal', 'elevated'] as const).forEach((mode) =>
+ (['filled', 'tonal', 'elevated'] as const).forEach((mode) =>
it(`should return correct disabled color, for theme version 3, dark theme, ${mode} mode`, () => {
return expect(
getButtonColors({
@@ -397,44 +533,44 @@ describe('getButtonColors - background color', () => {
});
});
- it('should return correct theme color, for theme version 3, contained mode', () => {
+ it('should return correct theme color, for theme version 3, filled mode', () => {
expect(
getButtonColors({
theme: getTheme(),
- mode: 'contained',
+ mode: 'filled',
})
).toMatchObject({
backgroundColor: getTheme().colors.primary,
});
});
- it('should return correct theme color, for theme version 3, dark theme, contained mode', () => {
+ it('should return correct theme color, for theme version 3, dark theme, filled mode', () => {
expect(
getButtonColors({
theme: getTheme(true),
- mode: 'contained',
+ mode: 'filled',
})
).toMatchObject({
backgroundColor: getTheme(true).colors.primary,
});
});
- it('should return correct theme color, for theme version 3, contained-tonal mode', () => {
+ it('should return correct theme color, for theme version 3, tonal mode', () => {
expect(
getButtonColors({
theme: getTheme(),
- mode: 'contained-tonal',
+ mode: 'tonal',
})
).toMatchObject({
backgroundColor: getTheme().colors.secondaryContainer,
});
});
- it('should return correct theme color, for theme version 3, dark theme, contained-tonal mode', () => {
+ it('should return correct theme color, for theme version 3, dark theme, tonal mode', () => {
expect(
getButtonColors({
theme: getTheme(true),
- mode: 'contained-tonal',
+ mode: 'tonal',
})
).toMatchObject({
backgroundColor: getTheme(true).colors.secondaryContainer,
@@ -469,48 +605,48 @@ describe('getButtonColors - background color', () => {
});
describe('getButtonColors - text color', () => {
- const customTextColor = '#313131';
+ const customLabelColor = '#313131';
it('should return custom text color no matter what is the theme version, when not disabled', () => {
expect(
getButtonColors({
- customTextColor,
+ customLabelColor,
theme: getTheme(),
disabled: false,
mode: 'text',
})
- ).toMatchObject({ textColor: customTextColor });
+ ).toMatchObject({ labelColor: customLabelColor });
});
it('should return correct disabled text color, for theme version 3, no matter what the mode is', () => {
expect(
getButtonColors({
- customTextColor,
+ customLabelColor,
theme: getTheme(),
disabled: true,
mode: 'text',
})
).toMatchObject({
- textColor: getTheme().colors.onSurface,
- textOpacity: stateOpacity.disabled,
+ labelColor: getTheme().colors.onSurface,
+ labelOpacity: stateOpacity.disabled,
});
});
it('should return correct disabled text color, for theme version 3, dark theme, no matter what the mode is', () => {
expect(
getButtonColors({
- customTextColor,
+ customLabelColor,
theme: getTheme(true),
disabled: true,
mode: 'text',
})
).toMatchObject({
- textColor: getTheme(true).colors.onSurface,
- textOpacity: stateOpacity.disabled,
+ labelColor: getTheme(true).colors.onSurface,
+ labelOpacity: stateOpacity.disabled,
});
});
- (['contained', 'contained-tonal', 'elevated'] as const).forEach((mode) =>
+ (['filled', 'tonal', 'elevated'] as const).forEach((mode) =>
it(`should return correct text color for dark prop, for theme version 3, ${mode} mode`, () => {
expect(
getButtonColors({
@@ -519,12 +655,12 @@ describe('getButtonColors - text color', () => {
dark: true,
})
).toMatchObject({
- textColor: white,
+ labelColor: white,
});
})
);
- (['outlined', 'text', 'elevated'] as const).forEach((mode) =>
+ (['text', 'elevated'] as const).forEach((mode) =>
it(`should return correct theme text color, for theme version 3, ${mode} mode`, () => {
expect(
getButtonColors({
@@ -532,12 +668,12 @@ describe('getButtonColors - text color', () => {
mode,
})
).toMatchObject({
- textColor: getTheme().colors.primary,
+ labelColor: getTheme().colors.primary,
});
})
);
- (['outlined', 'text', 'elevated'] as const).forEach((mode) =>
+ (['text', 'elevated'] as const).forEach((mode) =>
it(`should return correct theme text color, for theme version 3, dark theme, ${mode} mode`, () => {
expect(
getButtonColors({
@@ -545,52 +681,74 @@ describe('getButtonColors - text color', () => {
mode,
})
).toMatchObject({
- textColor: getTheme(true).colors.primary,
+ labelColor: getTheme(true).colors.primary,
});
})
);
- it('should return correct theme text color, for theme version 3, contained mode', () => {
+ it('should return onSurfaceVariant label color, for theme version 3, outlined mode', () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode: 'outlined',
+ })
+ ).toMatchObject({
+ labelColor: getTheme().colors.onSurfaceVariant,
+ });
+ });
+
+ it('should return onSurfaceVariant label color, for theme version 3, dark theme, outlined mode', () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(true),
+ mode: 'outlined',
+ })
+ ).toMatchObject({
+ labelColor: getTheme(true).colors.onSurfaceVariant,
+ });
+ });
+
+ it('should return correct theme text color, for theme version 3, filled mode', () => {
expect(
getButtonColors({
theme: getTheme(),
- mode: 'contained',
+ mode: 'filled',
})
).toMatchObject({
- textColor: getTheme().colors.onPrimary,
+ labelColor: getTheme().colors.onPrimary,
});
});
- it('should return correct theme text color, for theme version 3, dark theme, contained mode', () => {
+ it('should return correct theme text color, for theme version 3, dark theme, filled mode', () => {
expect(
getButtonColors({
theme: getTheme(true),
- mode: 'contained',
+ mode: 'filled',
})
).toMatchObject({
- textColor: getTheme(true).colors.onPrimary,
+ labelColor: getTheme(true).colors.onPrimary,
});
});
- it('should return correct theme text color, for theme version 3, contained-tonal mode', () => {
+ it('should return correct theme text color, for theme version 3, tonal mode', () => {
expect(
getButtonColors({
theme: getTheme(),
- mode: 'contained-tonal',
+ mode: 'tonal',
})
).toMatchObject({
- textColor: getTheme().colors.onSecondaryContainer,
+ labelColor: getTheme().colors.onSecondaryContainer,
});
});
- it('should return correct theme text color, for theme version 3, dark theme contained-tonal mode', () => {
+ it('should return correct theme text color, for theme version 3, dark theme tonal mode', () => {
expect(
getButtonColors({
theme: getTheme(true),
- mode: 'contained-tonal',
+ mode: 'tonal',
})
).toMatchObject({
- textColor: getTheme(true).colors.onSecondaryContainer,
+ labelColor: getTheme(true).colors.onSecondaryContainer,
});
});
});
@@ -604,7 +762,7 @@ describe('getButtonColors - border color', () => {
mode: 'outlined',
})
).toMatchObject({
- borderColor: getTheme().colors.outlineVariant,
+ borderColor: getTheme().colors.outline,
});
});
@@ -616,7 +774,7 @@ describe('getButtonColors - border color', () => {
mode: 'outlined',
})
).toMatchObject({
- borderColor: getTheme(true).colors.outlineVariant,
+ borderColor: getTheme(true).colors.outline,
});
});
@@ -627,7 +785,7 @@ describe('getButtonColors - border color', () => {
mode: 'outlined',
})
).toMatchObject({
- borderColor: getTheme().colors.outlineVariant,
+ borderColor: getTheme().colors.outline,
});
});
@@ -638,36 +796,34 @@ describe('getButtonColors - border color', () => {
mode: 'outlined',
})
).toMatchObject({
- borderColor: getTheme(true).colors.outlineVariant,
+ borderColor: getTheme(true).colors.outline,
});
});
- (['text', 'contained', 'contained-tonal', 'elevated'] as const).forEach(
- (mode) =>
- it(`should return transparent border, for theme version 3, ${mode} mode`, () => {
- expect(
- getButtonColors({
- theme: getTheme(),
- mode,
- })
- ).toMatchObject({
- borderColor: 'transparent',
- });
- })
+ (['text', 'filled', 'tonal', 'elevated'] as const).forEach((mode) =>
+ it(`should return transparent border, for theme version 3, ${mode} mode`, () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode,
+ })
+ ).toMatchObject({
+ borderColor: 'transparent',
+ });
+ })
);
- (['text', 'contained', 'contained-tonal', 'elevated'] as const).forEach(
- (mode) =>
- it(`should return transparent border, for theme version 3, dark theme, ${mode} mode`, () => {
- expect(
- getButtonColors({
- theme: getTheme(true),
- mode,
- })
- ).toMatchObject({
- borderColor: 'transparent',
- });
- })
+ (['text', 'filled', 'tonal', 'elevated'] as const).forEach((mode) =>
+ it(`should return transparent border, for theme version 3, dark theme, ${mode} mode`, () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(true),
+ mode,
+ })
+ ).toMatchObject({
+ borderColor: 'transparent',
+ });
+ })
);
});
@@ -683,21 +839,260 @@ describe('getButtonColors - border width', () => {
});
});
- (['text', 'contained', 'contained-tonal', 'elevated'] as const).forEach(
- (mode) =>
- it(`should return correct border width, for ${mode} mode`, () => {
- expect(
- getButtonColors({
- theme: getTheme(),
- mode,
- })
- ).toMatchObject({
- borderWidth: 0,
- });
- })
+ (['text', 'filled', 'tonal', 'elevated'] as const).forEach((mode) =>
+ it(`should return correct border width, for ${mode} mode`, () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode,
+ })
+ ).toMatchObject({
+ borderWidth: 0,
+ });
+ })
);
});
+describe('getButtonRippleColor', () => {
+ it('returns the custom ripple color when one is provided', () => {
+ expect(
+ getButtonRippleColor({ labelColor: '#123456', customRippleColor: 'red' })
+ ).toBe('red');
+ });
+
+ it('defaults to the label color at the pressed-state opacity', () => {
+ expect(getButtonRippleColor({ labelColor: '#123456' })).toBe(
+ color('#123456').alpha(stateOpacity.pressed).rgb().string()
+ );
+ });
+
+ it('returns undefined when the label color is not a plain string', () => {
+ expect(
+ getButtonRippleColor({ labelColor: PlatformColor('?attr/colorPrimary') })
+ ).toBeUndefined();
+ });
+});
+
+describe('getButtonSizeStyle', () => {
+ it.each([
+ ['extra-small', 32, 12, 20, 4, 'labelLarge'],
+ ['small', 40, 16, 20, 8, 'labelLarge'],
+ ['medium', 56, 24, 24, 8, 'titleMedium'],
+ ['large', 96, 48, 32, 12, 'headlineSmall'],
+ ['extra-large', 136, 64, 40, 16, 'headlineLarge'],
+ ] as const)(
+ 'returns expected metrics for %s',
+ (size, minHeight, paddingHorizontal, iconSize, iconGap, labelVariant) => {
+ expect(getButtonSizeStyle(size)).toEqual({
+ minHeight,
+ paddingHorizontal,
+ iconSize,
+ iconGap,
+ labelVariant,
+ });
+ }
+ );
+});
+
+describe('size prop', () => {
+ it('renders a button with per-size metrics', () => {
+ const tree = render(
+
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ (
+ [
+ ['extra-small', 14],
+ ['small', 14],
+ ['medium', 16],
+ ['large', 24],
+ ['extra-large', 32],
+ ] as const
+ ).forEach(([size, expectedFontSize]) =>
+ it(`applies the ${size} typescale to the label`, () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button-text')).toHaveStyle({
+ fontSize: expectedFontSize,
+ });
+ })
+ );
+});
+
+describe('accessible touch target', () => {
+ it('expands extra-small buttons to the 48dp minimum target', () => {
+ const { getByTestId } = render(
+
+ );
+ // (48 - 32) / 2 = 8
+ expect(getByTestId('button').props.hitSlop).toMatchObject({
+ top: 8,
+ bottom: 8,
+ });
+ });
+
+ it('expands small buttons to the 48dp minimum target', () => {
+ const { getByTestId } = render(
+
+ );
+ // (48 - 40) / 2 = 4
+ expect(getByTestId('button').props.hitSlop).toMatchObject({
+ top: 4,
+ bottom: 4,
+ });
+ });
+
+ it('does not add hitSlop for buttons already at least 48dp tall', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button').props.hitSlop).toBeUndefined();
+ });
+
+ it('keeps a user-supplied hitSlop axis while filling the rest', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button').props.hitSlop).toMatchObject({
+ top: 20,
+ bottom: 8,
+ });
+ });
+});
+
+describe('getButtonShapeRadius', () => {
+ it.each([
+ ['extra-small', 9999, 12],
+ ['small', 9999, 12],
+ ['medium', 9999, 16],
+ ['large', 9999, 28],
+ ['extra-large', 9999, 28],
+ ] as const)('returns expected radii for size=%s', (size, round, square) => {
+ const theme = getTheme();
+ expect(getButtonShapeRadius({ size, shape: 'round', theme })).toBe(round);
+ expect(getButtonShapeRadius({ size, shape: 'square', theme })).toBe(square);
+ });
+
+ it('falls back to default radii when size is omitted', () => {
+ const theme = getTheme();
+ expect(getButtonShapeRadius({ shape: 'round', theme })).toBe(9999);
+ expect(getButtonShapeRadius({ shape: 'square', theme })).toBe(12);
+ });
+});
+
+describe('shape prop', () => {
+ it('applies the round (full-pill) radius', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 9999 });
+ });
+
+ it('applies the square radius (default size)', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 12 });
+ });
+
+ it('uses the per-size square radius when both size and shape are set', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 28 });
+ });
+
+ it('lets an explicit borderRadius in `style` override the shape', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 4 });
+ });
+});
+
+describe('selected prop', () => {
+ it('sets accessibilityState.selected', () => {
+ const { getByTestId } = render(
+ {}} label="X" />
+ );
+
+ expect(getByTestId('button').props.accessibilityState).toMatchObject({
+ selected: true,
+ });
+ });
+
+ it('flips a round button into the square radius when selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 28 });
+ });
+
+ it('flips a square button into the round radius when selected', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 9999 });
+ });
+
+ it('gives an outlined button the tonal-selected appearance', () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode: 'outlined',
+ selected: true,
+ })
+ ).toMatchObject({
+ backgroundColor: getTheme().colors.secondaryContainer,
+ labelColor: getTheme().colors.onSecondaryContainer,
+ borderColor: 'transparent',
+ borderWidth: 0,
+ });
+ });
+
+ it('gives a text-mode button the tonal-selected appearance', () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode: 'text',
+ selected: true,
+ })
+ ).toMatchObject({
+ backgroundColor: getTheme().colors.secondaryContainer,
+ labelColor: getTheme().colors.onSecondaryContainer,
+ });
+ });
+
+ it('does not change filled colors when selected', () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode: 'filled',
+ selected: true,
+ })
+ ).toMatchObject({
+ backgroundColor: getTheme().colors.primary,
+ labelColor: getTheme().colors.onPrimary,
+ });
+ });
+});
+
it('animated value changes correctly', () => {
const value = new Animated.Value(1);
const { getByTestId } = render(
@@ -706,9 +1101,8 @@ it('animated value changes correctly', () => {
compact
icon="camera"
style={[{ transform: [{ scale: value }] }]}
- >
- Compact button
-
+ label="Compact button"
+ />
);
expect(getByTestId('button-container-outer-layer')).toHaveStyle({
transform: [{ scale: 1 }],
@@ -727,3 +1121,101 @@ it('animated value changes correctly', () => {
transform: [{ scale: 1.5 }],
});
});
+
+describe('shape morph animation', () => {
+ const lastSpringToValue = (spy: jest.SpyInstance) =>
+ spy.mock.calls.map((call) => call[1]?.toValue);
+
+ it('springs the corner radius to corner.small on press in', () => {
+ const spy = jest.spyOn(Animated, 'spring');
+ const { getByTestId } = render(
+ {}} testID="button" />
+ );
+ spy.mockClear();
+ fireEvent(getByTestId('button'), 'onPressIn');
+ expect(lastSpringToValue(spy)).toContain(getTheme().shapes.corner.small);
+ spy.mockRestore();
+ });
+
+ it('springs the corner radius back to the resting pill radius on press out', () => {
+ const spy = jest.spyOn(Animated, 'spring');
+ const { getByTestId } = render(
+ {}} testID="button" />
+ );
+ spy.mockClear();
+ fireEvent(getByTestId('button'), 'onPressOut');
+ // small round resting radius = minHeight (40) / 2 = 20
+ expect(lastSpringToValue(spy)).toContain(20);
+ spy.mockRestore();
+ });
+
+ it('animates between round and square radii when toggled (no spring on mount)', () => {
+ const spy = jest.spyOn(Animated, 'spring');
+ const { rerender } = render(
+ {}} testID="button" />
+ );
+ // Mount snaps to the resting radius — no spring.
+ expect(spy).not.toHaveBeenCalled();
+ rerender(
+ {}}
+ testID="button"
+ />
+ );
+ // selected flips square -> round; large round resting radius = 96 / 2 = 48
+ expect(lastSpringToValue(spy)).toContain(48);
+ spy.mockRestore();
+ });
+
+ it('does not morph legacy or size-only buttons', () => {
+ const spy = jest.spyOn(Animated, 'spring');
+ const { getByTestId, rerender } = render(
+ {}} testID="button" label="Legacy" />
+ );
+ spy.mockClear();
+ fireEvent(getByTestId('button'), 'onPressIn');
+ expect(spy).not.toHaveBeenCalled();
+
+ rerender(
+ {}} testID="button" label="Sized" />
+ );
+ spy.mockClear();
+ fireEvent(getByTestId('button'), 'onPressIn');
+ expect(spy).not.toHaveBeenCalled();
+ spy.mockRestore();
+ });
+
+ it('does not morph when the user pins a radius via style', () => {
+ const spy = jest.spyOn(Animated, 'spring');
+ const { getByTestId } = render(
+ {}}
+ testID="button"
+ />
+ );
+ spy.mockClear();
+ fireEvent(getByTestId('button'), 'onPressIn');
+ expect(spy).not.toHaveBeenCalled();
+ expect(getByTestId('button-container')).toHaveStyle({ borderRadius: 4 });
+ spy.mockRestore();
+ });
+
+ it('applies the pressed corner radius to the surface', () => {
+ const { getByTestId } = render(
+ {}} testID="button" />
+ );
+ fireEvent(getByTestId('button'), 'onPressIn');
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ expect(getByTestId('button-container')).toHaveStyle({
+ borderRadius: getTheme().shapes.corner.small,
+ });
+ });
+});
diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx
index b11e006445..5439e4148d 100644
--- a/src/components/__tests__/Card/Card.test.tsx
+++ b/src/components/__tests__/Card/Card.test.tsx
@@ -132,13 +132,13 @@ describe('CardActions', () => {
const { getByTestId } = render(
- Agree
+
);
expect(getByTestId('card-actions').props.children[0].props.mode).toBe(
- 'contained'
+ 'filled'
);
});
@@ -148,11 +148,10 @@ describe('CardActions', () => {
- Agree
-
+ label="Agree"
+ />
);
diff --git a/src/components/__tests__/Checkbox/utils.test.tsx b/src/components/__tests__/Checkbox/utils.test.tsx
index a18dd2f78e..2aa20fd688 100644
--- a/src/components/__tests__/Checkbox/utils.test.tsx
+++ b/src/components/__tests__/Checkbox/utils.test.tsx
@@ -87,6 +87,82 @@ describe('getAndroidSelectionControlColor - checkbox color', () => {
selectionControlColor: getTheme(false).colors.onSurfaceVariant,
});
});
+
+ it('should return error color, checked, when error is true', () => {
+ expect(
+ getAndroidSelectionControlColor({
+ theme: getTheme(),
+ checked: true,
+ error: true,
+ })
+ ).toMatchObject({
+ selectionControlColor: getTheme().colors.error,
+ });
+ });
+
+ it('should return error color, unchecked, when error is true', () => {
+ expect(
+ getAndroidSelectionControlColor({
+ theme: getTheme(),
+ checked: false,
+ error: true,
+ })
+ ).toMatchObject({
+ selectionControlColor: getTheme().colors.error,
+ });
+ });
+
+ it('should return error color, checked, dark mode, when error is true', () => {
+ expect(
+ getAndroidSelectionControlColor({
+ theme: getTheme(true),
+ checked: true,
+ error: true,
+ })
+ ).toMatchObject({
+ selectionControlColor: getTheme(true).colors.error,
+ });
+ });
+
+ it('should return disabled color when both disabled and error are true (disabled wins)', () => {
+ expect(
+ getAndroidSelectionControlColor({
+ theme: getTheme(),
+ disabled: true,
+ checked: true,
+ error: true,
+ })
+ ).toMatchObject({
+ selectionControlColor: getTheme().colors.onSurface,
+ selectionControlOpacity: stateOpacity.disabled,
+ });
+ });
+
+ it('should return custom color when both customColor and error are true, checked (customColor wins)', () => {
+ expect(
+ getAndroidSelectionControlColor({
+ theme: getTheme(),
+ checked: true,
+ customColor: 'purple',
+ error: true,
+ })
+ ).toMatchObject({
+ selectionControlColor: 'purple',
+ });
+ });
+
+ it('should return custom unchecked color when both customUncheckedColor and error are true, unchecked (customUncheckedColor wins)', () => {
+ expect(
+ getAndroidSelectionControlColor({
+ theme: getTheme(),
+ checked: false,
+ customUncheckedColor: 'purple',
+ error: true,
+ })
+ ).toMatchObject({
+ selectionControlColor: 'purple',
+ });
+ });
});
describe('getSelectionControlIOSColor - checked color', () => {
@@ -122,4 +198,51 @@ describe('getSelectionControlIOSColor - checked color', () => {
checkedColor: getTheme().colors.primary,
});
});
+
+ it('should return error color when error is true', () => {
+ expect(
+ getSelectionControlIOSColor({
+ theme: getTheme(),
+ error: true,
+ })
+ ).toMatchObject({
+ checkedColor: getTheme().colors.error,
+ });
+ });
+
+ it('should return error color, dark mode, when error is true', () => {
+ expect(
+ getSelectionControlIOSColor({
+ theme: getTheme(true),
+ error: true,
+ })
+ ).toMatchObject({
+ checkedColor: getTheme(true).colors.error,
+ });
+ });
+
+ it('should return disabled color when both disabled and error are true (disabled wins)', () => {
+ expect(
+ getSelectionControlIOSColor({
+ theme: getTheme(),
+ disabled: true,
+ error: true,
+ })
+ ).toMatchObject({
+ checkedColor: getTheme().colors.primary,
+ checkedColorOpacity: stateOpacity.disabled,
+ });
+ });
+
+ it('should return custom color when both customColor and error are true (customColor wins)', () => {
+ expect(
+ getSelectionControlIOSColor({
+ theme: getTheme(),
+ customColor: 'purple',
+ error: true,
+ })
+ ).toMatchObject({
+ checkedColor: 'purple',
+ });
+ });
});
diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx
index 742f44b941..47a6409c58 100644
--- a/src/components/__tests__/Dialog.test.tsx
+++ b/src/components/__tests__/Dialog.test.tsx
@@ -110,8 +110,8 @@ describe('DialogActions', () => {
it('should render passed children', () => {
const { getByTestId } = render(
- Cancel
- Ok
+
+
);
@@ -122,8 +122,8 @@ describe('DialogActions', () => {
it('should apply default styles', () => {
const { getByTestId } = render(
- Cancel
- Ok
+
+
);
@@ -141,8 +141,8 @@ describe('DialogActions', () => {
it('should apply custom styles', () => {
const { getByTestId } = render(
- Cancel
- Ok
+
+
);
diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx
index 9147ad456a..e71cb70d89 100644
--- a/src/components/__tests__/Menu.test.tsx
+++ b/src/components/__tests__/Menu.test.tsx
@@ -23,7 +23,7 @@ it('renders visible menu', () => {
Open menu }
+ anchor={ }
>
@@ -40,7 +40,7 @@ it('renders not visible menu', () => {
Open menu }
+ anchor={ }
>
@@ -57,7 +57,7 @@ it('renders menu with content styles', () => {
Open menu }
+ anchor={ }
contentStyle={styles.contentStyle}
>
@@ -78,7 +78,7 @@ it('renders menu with content styles', () => {
Open menu }
+ anchor={ }
elevation={elevation}
>
@@ -110,11 +110,7 @@ it('uses the default anchorPosition of top', async () => {
- Open menu
-
- }
+ anchor={ }
contentStyle={styles.contentStyle}
>
@@ -166,11 +162,7 @@ it('respects anchorPosition bottom', async () => {
- Open menu
-
- }
+ anchor={ }
anchorPosition="bottom"
contentStyle={styles.contentStyle}
>
@@ -209,7 +201,7 @@ it('animated value changes correctly', () => {
Open menu }
+ anchor={ }
testID="menu"
contentStyle={[{ transform: [{ scale: value }] }]}
>
@@ -241,7 +233,7 @@ it('renders menu with mode "elevated"', () => {
Open menu }
+ anchor={ }
mode="elevated"
>
@@ -265,7 +257,7 @@ it('renders menu with mode "flat"', () => {
Open menu}
+ anchor={ }
mode="flat"
>
diff --git a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap
index 2b7d4fd813..c27ef0c407 100644
--- a/src/components/__tests__/__snapshots__/Banner.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Banner.test.tsx.snap
@@ -208,6 +208,7 @@ exports[`render visible banner, with custom theme 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -641,6 +642,7 @@ exports[`renders visible banner, with action buttons and with image 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -917,6 +919,7 @@ exports[`renders visible banner, with action buttons and without image 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -1069,6 +1072,7 @@ exports[`renders visible banner, with action buttons and without image 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
diff --git a/src/components/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/__tests__/__snapshots__/Button.test.tsx.snap
index 992205a103..1d96d9a7db 100644
--- a/src/components/__tests__/__snapshots__/Button.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Button.test.tsx.snap
@@ -5,7 +5,7 @@ exports[`renders button with an accessibility hint 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -22,7 +22,7 @@ exports[`renders button with an accessibility hint 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -92,6 +92,7 @@ exports[`renders button with an accessibility hint 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -126,12 +127,13 @@ exports[`renders button with an accessibility hint 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 12,
+ "marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -158,7 +160,7 @@ exports[`renders button with an accessibility label 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -175,7 +177,7 @@ exports[`renders button with an accessibility label 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -245,6 +247,7 @@ exports[`renders button with an accessibility label 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -279,12 +282,13 @@ exports[`renders button with an accessibility label 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 12,
+ "marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -397,6 +401,7 @@ exports[`renders button with button color 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -431,12 +436,13 @@ exports[`renders button with button color 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 12,
+ "marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -463,7 +469,7 @@ exports[`renders button with color 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -480,7 +486,7 @@ exports[`renders button with color 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -549,6 +555,7 @@ exports[`renders button with color 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -583,7 +590,8 @@ exports[`renders button with color 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 12,
+ "marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
@@ -615,7 +623,7 @@ exports[`renders button with custom testID 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -632,7 +640,7 @@ exports[`renders button with custom testID 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -701,6 +709,7 @@ exports[`renders button with custom testID 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -735,12 +744,13 @@ exports[`renders button with custom testID 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 12,
+ "marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -767,7 +777,7 @@ exports[`renders button with icon 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -784,7 +794,7 @@ exports[`renders button with icon 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -853,6 +863,7 @@ exports[`renders button with icon 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -862,20 +873,10 @@ exports[`renders button with icon 1`] = `
>
@@ -887,12 +888,12 @@ exports[`renders button with icon 1`] = `
style={
[
{
- "color": "rgba(103, 80, 164, 1)",
- "fontSize": 18,
+ "color": "rgba(255, 255, 255, 1)",
+ "fontSize": 20,
},
[
{
- "lineHeight": 18,
+ "lineHeight": 20,
"transform": [
{
"scaleX": 1,
@@ -937,11 +938,12 @@ exports[`renders button with icon 1`] = `
},
{
"marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -968,7 +970,7 @@ exports[`renders button with icon in reverse order 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -985,7 +987,7 @@ exports[`renders button with icon in reverse order 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -1054,6 +1056,9 @@ exports[`renders button with icon in reverse order 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ {
+ "flexDirection": "row-reverse",
+ },
{
"opacity": 1,
},
@@ -1065,20 +1070,10 @@ exports[`renders button with icon in reverse order 1`] = `
>
@@ -1090,12 +1085,12 @@ exports[`renders button with icon in reverse order 1`] = `
style={
[
{
- "color": "rgba(103, 80, 164, 1)",
- "fontSize": 18,
+ "color": "rgba(255, 255, 255, 1)",
+ "fontSize": 20,
},
[
{
- "lineHeight": 18,
+ "lineHeight": 20,
"transform": [
{
"scaleX": 1,
@@ -1140,11 +1135,12 @@ exports[`renders button with icon in reverse order 1`] = `
},
{
"marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -1166,7 +1162,180 @@ exports[`renders button with icon in reverse order 1`] = `
`;
-exports[`renders contained contained with mode 1`] = `
+exports[`renders disabled button 1`] = `
+
+
+
+
+
+
+ Disabled Button
+
+
+
+
+
+`;
+
+exports[`renders filled button by default 1`] = `
- Contained Button
+ Filled Button
@@ -1319,12 +1489,12 @@ exports[`renders contained contained with mode 1`] = `
`;
-exports[`renders disabled button 1`] = `
+exports[`renders filled button with mode 1`] = `
- Disabled Button
+ Contained Button
@@ -1476,7 +1648,7 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderRadius": 20,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
@@ -1493,7 +1665,7 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "transparent",
+ "backgroundColor": "rgba(103, 80, 164, 1)",
"borderColor": "transparent",
"borderRadius": 20,
"borderStyle": "solid",
@@ -1562,6 +1734,7 @@ exports[`renders loading button 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -1583,20 +1756,10 @@ exports[`renders loading button 1`] = `
"alignItems": "center",
"justifyContent": "center",
},
- [
- {
- "marginLeft": 12,
- "marginRight": -4,
- },
- {
- "marginLeft": 16,
- "marginRight": -16,
- },
- {
- "marginLeft": 12,
- "marginRight": -8,
- },
- ],
+ {
+ "marginLeft": 16,
+ "marginRight": -8,
+ },
]
}
>
@@ -1604,9 +1767,9 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
+ "height": 20,
"opacity": 1,
- "width": 18,
+ "width": 20,
}
}
>
@@ -1628,13 +1791,13 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
+ "height": 20,
"transform": [
{
"rotate": "45deg",
},
],
- "width": 18,
+ "width": 20,
}
}
>
@@ -1642,9 +1805,9 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
+ "height": 10,
"overflow": "hidden",
- "width": 18,
+ "width": 20,
}
}
>
@@ -1652,7 +1815,7 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
+ "height": 20,
"transform": [
{
"translateY": 0,
@@ -1661,7 +1824,7 @@ exports[`renders loading button 1`] = `
"rotate": "-165deg",
},
],
- "width": 18,
+ "width": 20,
}
}
>
@@ -1669,9 +1832,9 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
+ "height": 10,
"overflow": "hidden",
- "width": 18,
+ "width": 20,
}
}
>
@@ -1679,11 +1842,11 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "borderColor": "rgba(103, 80, 164, 1)",
- "borderRadius": 9,
- "borderWidth": 1.8,
- "height": 18,
- "width": 18,
+ "borderColor": "rgba(255, 255, 255, 1)",
+ "borderRadius": 10,
+ "borderWidth": 2,
+ "height": 20,
+ "width": 20,
}
}
/>
@@ -1710,13 +1873,13 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
+ "height": 20,
"transform": [
{
"rotate": "45deg",
},
],
- "width": 18,
+ "width": 20,
}
}
>
@@ -1724,10 +1887,10 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
+ "height": 10,
"overflow": "hidden",
- "top": 9,
- "width": 18,
+ "top": 10,
+ "width": 20,
}
}
>
@@ -1735,16 +1898,16 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 18,
+ "height": 20,
"transform": [
{
- "translateY": -9,
+ "translateY": -10,
},
{
"rotate": "345deg",
},
],
- "width": 18,
+ "width": 20,
}
}
>
@@ -1752,9 +1915,9 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "height": 9,
+ "height": 10,
"overflow": "hidden",
- "width": 18,
+ "width": 20,
}
}
>
@@ -1762,11 +1925,11 @@ exports[`renders loading button 1`] = `
collapsable={false}
style={
{
- "borderColor": "rgba(103, 80, 164, 1)",
- "borderRadius": 9,
- "borderWidth": 1.8,
- "height": 18,
- "width": 18,
+ "borderColor": "rgba(255, 255, 255, 1)",
+ "borderRadius": 10,
+ "borderWidth": 2,
+ "height": 20,
+ "width": 20,
}
}
/>
@@ -1805,11 +1968,12 @@ exports[`renders loading button 1`] = `
},
{
"marginHorizontal": 16,
+ "marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(255, 255, 255, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -1854,7 +2018,7 @@ exports[`renders outlined button with mode 1`] = `
style={
{
"backgroundColor": "transparent",
- "borderColor": "rgba(202, 196, 208, 1)",
+ "borderColor": "rgba(121, 116, 126, 1)",
"borderRadius": 20,
"borderStyle": "solid",
"borderWidth": 1,
@@ -1922,6 +2086,7 @@ exports[`renders outlined button with mode 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -1956,13 +2121,13 @@ exports[`renders outlined button with mode 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 24,
+ "marginHorizontal": 16,
"marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(73, 69, 79, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -1984,7 +2149,7 @@ exports[`renders outlined button with mode 1`] = `
`;
-exports[`renders text button by default 1`] = `
+exports[`renders text button with mode 1`] = `
`;
-exports[`renders text button with mode 1`] = `
+exports[`size prop renders a button with per-size metrics 1`] = `
+
+
+ camera
+
+
- Text Button
+ Medium
diff --git a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
index 0f997739ba..765b144ed9 100644
--- a/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/DataTable.test.tsx.snap
@@ -1718,7 +1718,7 @@ exports[`DataTable.Pagination renders data table pagination with options select
style={
{
"backgroundColor": "transparent",
- "borderColor": "rgba(202, 196, 208, 1)",
+ "borderColor": "rgba(121, 116, 126, 1)",
"borderRadius": 20,
"borderStyle": "solid",
"borderWidth": 1,
@@ -1788,27 +1788,21 @@ exports[`DataTable.Pagination renders data table pagination with options select
"justifyContent": "center",
},
{
- "opacity": 1,
+ "flexDirection": "row-reverse",
},
{
- "flexDirection": "row-reverse",
+ "opacity": 1,
},
+ undefined,
]
}
>
@@ -1820,12 +1814,12 @@ exports[`DataTable.Pagination renders data table pagination with options select
style={
[
{
- "color": "rgba(103, 80, 164, 1)",
- "fontSize": 18,
+ "color": "rgba(73, 69, 79, 1)",
+ "fontSize": 20,
},
[
{
- "lineHeight": 18,
+ "lineHeight": 20,
"transform": [
{
"scaleX": 1,
@@ -1869,13 +1863,13 @@ exports[`DataTable.Pagination renders data table pagination with options select
"textAlign": "center",
},
{
- "marginHorizontal": 24,
+ "marginHorizontal": 16,
"marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(73, 69, 79, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
diff --git a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap
index ee711c93f8..f7e198784b 100644
--- a/src/components/__tests__/__snapshots__/Menu.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Menu.test.tsx.snap
@@ -36,7 +36,7 @@ exports[`renders menu with content styles 1`] = `
style={
{
"backgroundColor": "transparent",
- "borderColor": "rgba(202, 196, 208, 1)",
+ "borderColor": "rgba(121, 116, 126, 1)",
"borderRadius": 20,
"borderStyle": "solid",
"borderWidth": 1,
@@ -104,6 +104,7 @@ exports[`renders menu with content styles 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -138,13 +139,13 @@ exports[`renders menu with content styles 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 24,
+ "marginHorizontal": 16,
"marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(73, 69, 79, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -599,7 +600,7 @@ exports[`renders not visible menu 1`] = `
style={
{
"backgroundColor": "transparent",
- "borderColor": "rgba(202, 196, 208, 1)",
+ "borderColor": "rgba(121, 116, 126, 1)",
"borderRadius": 20,
"borderStyle": "solid",
"borderWidth": 1,
@@ -667,6 +668,7 @@ exports[`renders not visible menu 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -701,13 +703,13 @@ exports[`renders not visible menu 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 24,
+ "marginHorizontal": 16,
"marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(73, 69, 79, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
@@ -767,7 +769,7 @@ exports[`renders visible menu 1`] = `
style={
{
"backgroundColor": "transparent",
- "borderColor": "rgba(202, 196, 208, 1)",
+ "borderColor": "rgba(121, 116, 126, 1)",
"borderRadius": 20,
"borderStyle": "solid",
"borderWidth": 1,
@@ -835,6 +837,7 @@ exports[`renders visible menu 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},
@@ -869,13 +872,13 @@ exports[`renders visible menu 1`] = `
"textAlign": "center",
},
{
- "marginHorizontal": 24,
+ "marginHorizontal": 16,
"marginVertical": 10,
},
undefined,
false,
{
- "color": "rgba(103, 80, 164, 1)",
+ "color": "rgba(73, 69, 79, 1)",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
diff --git a/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap
index bc4642cc3f..c808c020d3 100644
--- a/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Snackbar.test.tsx.snap
@@ -410,6 +410,7 @@ exports[`renders snackbar with action button 1`] = `
"flexDirection": "row",
"justifyContent": "center",
},
+ false,
{
"opacity": 1,
},