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 - + + label="Press me" +/> ``` Local image: ```js - + + 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 - + + 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 - + + 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 = () => { > - - - + + + 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. - + - - - - + + + label="Play me" + /> + + + + + + + {showIcon && ( + + )} + + + + {/* `compact` is a no-op once a size is set, so only offer it for unset. */} + {size === 'unset' && ( + + )} - + + - - - - - - + {MODES.map((m) => ( + + label="Enabled" + /> + label="Disabled" + /> - - + label="Loading" + /> - + + - - - - - - + {SIZES.filter( + (s): s is Exclude => s !== 'unset' + ).map((s) => ( + - - - - - + {(['outlined', 'text', 'tonal'] as const).map((m) => { + const key = `toggle-${m}`; + const isSelected = !!selectedToggles[key]; + return ( + + label="Custom color" + /> + label="Remote image" + /> + label="Custom component" + /> - + labelStyle={styles.fontStyles} + label="Custom font" + /> - - - - - - + label="Custom radius" + /> - - - - - {( - [ - 'text', - 'outlined', - 'contained', - 'elevated', - 'contained-tonal', - ] as const - ).map((mode) => { - return ( - - ); - })} + 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 = () => { - - + - + + label="Long text" + /> + label="Radio buttons" + /> + label="Progress indicator" + /> + label="Undismissable Dialog" + /> + label="Custom colors" + /> + label="With icon" + /> {Platform.OS === 'android' && ( + label="Dismissable back button" + /> )} - + - + - + + - + - + + + + - - + + label={showSnackbar ? 'Hide' : 'Show'} + /> { - - + - + + /> ))} 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 = () => ( - * + * - * + * - * + * + 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 ( * * - * + * + * - * + * }> + * anchor={ + * + * + /> ) : null} {isIconButton ? ( { - const tree = render().toJSON(); +it('renders filled button by default', () => { + const tree = render().toJSON(); + const tree = render( + + ).toJSON(); + const tree = render( + ).toJSON(); + const tree = render().toJSON(); + const tree = render( + + + + + + + + + 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( - + + + + ); + + 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( + + ); + + expect(getByTestId('button-text')).toHaveTextContent('Legacy label'); + warn.mockRestore(); + }); + + it('warns about the deprecation', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + render(); + + 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( - + + + - ); - 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: 16, - marginRight: -16, - }); - }) + (['outlined', 'filled', 'tonal', 'elevated'] as const).forEach((mode) => + it(`should return correct icon styles for compact ${mode} button`, () => { + const { getByTestId } = render( + + 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( + + + 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( - - + - + - + } + anchor={} + anchor={} + anchor={} + anchor={ - } + anchor={ - } + anchor={} + anchor={} + anchor={} + anchor={