diff --git a/docs/docs/guides/03-icons.mdx b/docs/docs/guides/03-icons.mdx
index 015a1bee1b..970177da49 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
-
+
```
:::note
@@ -63,15 +63,14 @@ Remote image:
```js
+ label="Press me"
+/>
```
Local image:
```js
-
+
```
### 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
-
+
```
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/09-react-navigation.md b/docs/docs/guides/09-react-navigation.md
index d9227cfb2b..18f5f20578 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
-
+
);
}
diff --git a/docs/docs/guides/11-ripple-effect.md b/docs/docs/guides/11-ripple-effect.md
index 31845060d7..fcdb1e70a4 100644
--- a/docs/docs/guides/11-ripple-effect.md
+++ b/docs/docs/guides/11-ripple-effect.md
@@ -19,9 +19,9 @@ The `rippleColor` prop is available for every pressable component which allows y
rippleColor="#FF000020"
icon="camera"
mode="contained"
- onPress={() => console.log('Pressed')}>
- Press me
-
+ 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 7f4acdc7bc..1bf609a20c 100644
--- a/docs/src/components/BannerExample.tsx
+++ b/docs/src/components/BannerExample.tsx
@@ -74,15 +74,19 @@ const BannerExample = () => {
>
-
-
-
+
+ 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 261b7ff9e6..b2fa86397a 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 d939d4e401..7d51071775 100644
--- a/example/src/Examples/Dialogs/DialogWithIcon.tsx
+++ b/example/src/Examples/Dialogs/DialogWithIcon.tsx
@@ -24,10 +24,8 @@ 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 ded4896fe2..1305a2584b 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 b43fb04778..4a1e8cfeb3 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 5ab797bb1d..1a146597f7 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 59cebe3a93..7e637315e5 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import {
AccessibilityRole,
Animated,
+ ColorValue,
GestureResponderEvent,
Platform,
PressableAndroidRippleConfig,
@@ -14,7 +15,13 @@ import {
import {
ButtonMode,
+ ButtonShape,
+ ButtonSize,
getButtonColors,
+ getButtonIconStyle,
+ getButtonRippleColor,
+ getButtonShapeRadius,
+ getButtonSizeStyle,
getButtonTouchableRippleStyle,
} from './utils';
import { useInternalTheme } from '../../core/theming';
@@ -30,7 +37,10 @@ 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.
@@ -50,6 +60,36 @@ export type Props = $Omit, 'mode'> & {
* 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 `contained-tonal`).
+ * - `accessibilityState.selected` is set so screen readers announce the
+ * toggle state.
+ *
+ * Other modes only flip the shape.
+ */
+ selected?: boolean;
/**
* @deprecated Deprecated in v5.x - use `buttonColor` or `textColor` instead.
* Custom text color for flat button, or background color for contained button.
@@ -71,6 +111,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.
*/
@@ -78,9 +122,14 @@ export type Props = $Omit, 'mode'> & {
/**
* Label text of the button.
*/
- children: React.ReactNode;
+ label?: string;
/**
- * Make the label text uppercased. Note that this won't work if you pass React elements as children.
+ * @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.
*/
uppercase?: boolean;
/**
@@ -88,6 +137,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.
*/
@@ -122,7 +176,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;
/**
@@ -161,24 +218,39 @@ 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;
+const iconSize = 18;
+
const Button = (
{
disabled,
compact,
mode = 'text',
+ size,
+ shape,
+ selected,
dark,
loading,
icon,
+ iconPosition,
buttonColor: customButtonColor,
textColor: customTextColor,
+ label,
children,
accessibilityLabel,
accessibilityHint,
@@ -197,6 +269,7 @@ const Button = (
testID = 'button',
accessible,
background,
+ rippleColor: customRippleColor,
maxFontSizeMultiplier,
touchableRef,
...rest
@@ -204,16 +277,34 @@ const Button = (
ref: React.ForwardedRef
) => {
const theme = useInternalTheme(themeOverrides);
- const isMode = React.useCallback(
- (modeToCompare: ButtonMode) => {
- return mode === modeToCompare;
- },
- [mode]
- );
+ 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 isTrailingIcon = iconPosition === 'trailing' || usesReverseContentStyle;
+
const hasPassedTouchHandler = hasTouchHandler({
onPress,
onPressIn,
@@ -222,8 +313,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)
@@ -237,42 +326,61 @@ const Button = (
duration: 0,
useNativeDriver: true,
});
- }, [isElevationEntitled, elevation, initialElevation]);
+ }, [isElevationEntitled, elevation]);
- const handlePressIn = (e: GestureResponderEvent) => {
- onPressIn?.(e);
- if (isMode('elevated')) {
- const { scale } = animation;
- Animated.timing(elevation, {
- toValue: activeElevation,
- duration: 200 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
- }
- };
-
- 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();
- }
- };
+ const handlePressIn = React.useCallback(
+ (e: GestureResponderEvent) => {
+ onPressIn?.(e);
+ if (mode === 'elevated') {
+ const { scale } = animation;
+ Animated.timing(elevation, {
+ toValue: activeElevation,
+ duration: 200 * scale,
+ useNativeDriver:
+ isWeb || Platform.constants.reactNativeVersion.minor <= 72,
+ }).start();
+ }
+ },
+ [onPressIn, mode, animation, elevation, isWeb]
+ );
- const flattenedStyles = (StyleSheet.flatten(style) || {}) as ViewStyle;
- const [, borderRadiusStyles] = splitStyles(
- flattenedStyles,
- (style) => style.startsWith('border') && style.endsWith('Radius')
+ const handlePressOut = React.useCallback(
+ (e: GestureResponderEvent) => {
+ onPressOut?.(e);
+ if (mode === 'elevated') {
+ const { scale } = animation;
+ Animated.timing(elevation, {
+ toValue: initialElevation,
+ duration: 150 * scale,
+ useNativeDriver:
+ isWeb || Platform.constants.reactNativeVersion.minor <= 72,
+ }).start();
+ }
+ },
+ [onPressOut, mode, animation, elevation, isWeb]
);
- const borderRadius = theme.shapes.corner.largeIncreased;
- const iconSize = 18;
+ 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 borderRadius = effectiveShape
+ ? getButtonShapeRadius({ size, shape: effectiveShape })
+ : theme.shapes.corner.largeIncreased;
const {
backgroundColor,
@@ -281,51 +389,83 @@ const Button = (
textOpacity,
borderWidth,
backgroundOpacity,
- } = getButtonColors({
- customButtonColor,
- customTextColor,
- theme,
- mode,
- disabled,
- dark,
- });
+ } = React.useMemo(
+ () =>
+ getButtonColors({
+ customButtonColor,
+ customTextColor,
+ theme,
+ mode,
+ disabled,
+ dark,
+ selected,
+ }),
+ [customButtonColor, customTextColor, theme, mode, disabled, dark, selected]
+ );
- const touchableStyle = {
- ...borderRadiusStyles,
- borderRadius: borderRadiusStyles.borderRadius ?? borderRadius,
- };
+ const rippleColor = React.useMemo(
+ () => getButtonRippleColor({ textColor, customRippleColor }),
+ [textColor, customRippleColor]
+ );
- const buttonStyle = {
- backgroundColor: backgroundOpacity < 1 ? 'transparent' : backgroundColor,
- borderColor,
- borderWidth,
- ...touchableStyle,
- };
+ const touchableStyle = React.useMemo(
+ () => ({
+ ...borderRadiusStyles,
+ borderRadius: borderRadiusStyles.borderRadius ?? borderRadius,
+ }),
+ [borderRadiusStyles, borderRadius]
+ );
- const { color: customLabelColor, fontSize: customLabelSize } =
- StyleSheet.flatten(labelStyle) || {};
+ const buttonStyle = React.useMemo(
+ () => ({
+ backgroundColor: backgroundOpacity < 1 ? 'transparent' : backgroundColor,
+ borderColor,
+ borderWidth,
+ ...touchableStyle,
+ }),
+ [
+ backgroundOpacity,
+ backgroundColor,
+ borderColor,
+ borderWidth,
+ touchableStyle,
+ ]
+ );
- const font = (theme as Theme).fonts.labelLarge;
+ const touchableRippleStyle = React.useMemo(
+ () => getButtonTouchableRippleStyle(touchableStyle, borderWidth),
+ [touchableStyle, borderWidth]
+ );
- const textStyle = {
- color: textColor,
- ...font,
- };
+ const { color: customLabelColor, fontSize: customLabelSize } = React.useMemo(
+ () => StyleSheet.flatten(labelStyle) || {},
+ [labelStyle]
+ );
- 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 sizeStyle = React.useMemo(
+ () => (size ? getButtonSizeStyle(size) : undefined),
+ [size]
+ );
+
+ const textStyle = React.useMemo(
+ () => ({
+ color: textColor,
+ ...(theme as Theme).fonts[sizeStyle?.labelVariant ?? 'labelLarge'],
+ }),
+ [textColor, theme, sizeStyle]
+ );
+
+ const iconStyle = React.useMemo(
+ () =>
+ sizeStyle
+ ? null
+ : getButtonIconStyle({
+ mode,
+ compact,
+ position: isTrailingIcon ? 'trailing' : 'leading',
+ }),
+ [mode, compact, isTrailingIcon, sizeStyle]
+ );
return (
-
+
{icon && loading !== true ? (
-
+
) : null}
- {children}
+ {labelContent}
@@ -441,53 +603,18 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
- icon: {
- marginLeft: 12,
- marginRight: -4,
- },
- iconReverse: {
- marginRight: 12,
- marginLeft: -4,
- },
- /* eslint-disable react-native/no-unused-styles */
- md3Icon: {
- marginLeft: 16,
- marginRight: -16,
- },
- md3IconCompact: {
- marginLeft: 8,
- marginRight: 0,
+ contentReverse: {
+ flexDirection: 'row-reverse',
},
- 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,
},
diff --git a/src/components/Button/utils.tsx b/src/components/Button/utils.tsx
index 37038afbd7..639c16bbf2 100644
--- a/src/components/Button/utils.tsx
+++ b/src/components/Button/utils.tsx
@@ -1,7 +1,10 @@
-import type { ViewStyle } from 'react-native';
+import type { ColorValue, ViewStyle } from 'react-native';
+
+import color from 'color';
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';
@@ -14,10 +17,142 @@ export type ButtonMode =
| 'elevated'
| 'contained-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.
+ * Used when the `size` prop is explicitly set; if `size` is omitted, the
+ * Button keeps its legacy visuals.
+ */
+const BUTTON_SIZE_STYLES: Record = {
+ 'extra-small': {
+ minHeight: 32,
+ paddingHorizontal: 12,
+ iconSize: 16,
+ iconGap: 4,
+ labelVariant: 'labelLarge',
+ },
+ small: {
+ minHeight: 40,
+ paddingHorizontal: 16,
+ iconSize: 20,
+ iconGap: 8,
+ labelVariant: 'labelLarge',
+ },
+ medium: {
+ minHeight: 56,
+ paddingHorizontal: 24,
+ iconSize: 24,
+ iconGap: 8,
+ labelVariant: 'titleMedium',
+ },
+ large: {
+ minHeight: 96,
+ paddingHorizontal: 48,
+ iconSize: 32,
+ iconGap: 12,
+ labelVariant: 'headlineSmall',
+ },
+ 'extra-large': {
+ minHeight: 136,
+ paddingHorizontal: 64,
+ iconSize: 40,
+ iconGap: 16,
+ labelVariant: 'headlineLarge',
+ },
+};
+
+export const getButtonSizeStyle = (size: ButtonSize): ButtonSizeStyle =>
+ BUTTON_SIZE_STYLES[size];
+
+export type ButtonShape = 'round' | 'square';
+
+/**
+ * Per-size corner radii for the Material Design 3 expressive shape variants.
+ * `round` is always the full-pill radius; `square` uses a per-size smaller
+ * corner. Used only when the `shape` prop is set on `Button`.
+ */
+const BUTTON_SHAPE_RADIUS: Record> = {
+ 'extra-small': { round: cornerFull, square: 12 },
+ small: { round: cornerFull, square: 12 },
+ medium: { round: cornerFull, square: 16 },
+ large: { round: cornerFull, square: 28 },
+ 'extra-large': { round: cornerFull, square: 28 },
+};
+
+const DEFAULT_SHAPE_RADIUS: Record = {
+ round: cornerFull,
+ square: 12,
+};
+
+export const getButtonShapeRadius = ({
+ size,
+ shape,
+}: {
+ size?: ButtonSize;
+ shape: ButtonShape;
+}): number =>
+ size ? BUTTON_SHAPE_RADIUS[size][shape] : DEFAULT_SHAPE_RADIUS[shape];
+
+/**
+ * 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: -16, marginRight: 16 };
+ }
+
+ if (compact) {
+ return { marginLeft: isTextMode ? 6 : 8, marginRight: 0 };
+ }
+ return isTextMode
+ ? { marginLeft: 12, marginRight: -8 }
+ : { marginLeft: 16, marginRight: -16 };
+};
+
type BaseProps = {
isMode: (mode: ButtonMode) => boolean;
theme: InternalTheme;
disabled?: boolean;
+ selected?: boolean;
};
const isDark = ({
@@ -43,6 +178,7 @@ const getButtonBackgroundColor = ({
theme,
disabled,
customButtonColor,
+ selected,
}: BaseProps & {
customButtonColor?: string;
}) => {
@@ -58,6 +194,12 @@ const getButtonBackgroundColor = ({
return colors.onSurface;
}
+ // Selected toggle (only outlined/text adopt a filled "tonal-selected" look;
+ // contained / contained-tonal / elevated already render filled).
+ if (selected && (isMode('outlined') || isMode('text'))) {
+ return colors.secondaryContainer;
+ }
+
if (isMode('elevated')) {
return colors.surfaceContainerLow;
}
@@ -80,6 +222,7 @@ const getButtonTextColor = ({
customTextColor,
backgroundColor,
dark,
+ selected,
}: BaseProps & {
customTextColor?: string;
backgroundColor: string;
@@ -94,6 +237,11 @@ const getButtonTextColor = ({
return theme.colors.onSurface;
}
+ // Selected toggle for outlined/text mirrors the contained-tonal label color.
+ if (selected && (isMode('outlined') || isMode('text'))) {
+ return colors.onSecondaryContainer;
+ }
+
if (typeof dark === 'boolean') {
if (
isMode('contained') ||
@@ -119,7 +267,12 @@ const getButtonTextColor = ({
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;
}
@@ -127,7 +280,13 @@ const getButtonBorderColor = ({ isMode, theme }: BaseProps) => {
return 'transparent';
};
-const getButtonBorderWidth = ({ isMode }: Omit) => {
+const getButtonBorderWidth = ({
+ isMode,
+ selected,
+}: Omit) => {
+ if (selected && isMode('outlined')) {
+ return 0;
+ }
if (isMode('outlined')) {
return 1;
}
@@ -142,6 +301,7 @@ export const getButtonColors = ({
customTextColor,
disabled,
dark,
+ selected,
}: {
theme: InternalTheme;
mode: ButtonMode;
@@ -149,6 +309,7 @@ export const getButtonColors = ({
customTextColor?: string;
disabled?: boolean;
dark?: boolean;
+ selected?: boolean;
}) => {
const isMode = (modeToCompare: ButtonMode) => {
return mode === modeToCompare;
@@ -159,6 +320,7 @@ export const getButtonColors = ({
theme,
disabled,
customButtonColor,
+ selected,
});
const textColor = getButtonTextColor({
@@ -168,11 +330,12 @@ export const getButtonColors = ({
customTextColor,
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;
@@ -191,6 +354,33 @@ export const getButtonColors = ({
};
};
+/**
+ * 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 = ({
+ textColor,
+ customRippleColor,
+}: {
+ textColor: ColorValue;
+ customRippleColor?: ColorValue;
+}): ColorValue | undefined => {
+ if (customRippleColor) {
+ return customRippleColor;
+ }
+
+ if (typeof textColor !== 'string') {
+ return undefined;
+ }
+
+ return color(textColor).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 c1522e5bf5..cc81efb1da 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 b34da0844b..e7ad471db2 100644
--- a/src/components/Card/CardActions.tsx
+++ b/src/components/Card/CardActions.tsx
@@ -26,8 +26,8 @@ export type Props = React.ComponentPropsWithRef & {
* const MyComponent = () => (
*
*
- * Cancel
- * Ok
+ *
+ *
*
*
* );
diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx
index 056bb432b9..2f799c71eb 100644
--- a/src/components/DataTable/DataTablePagination.tsx
+++ b/src/components/DataTable/DataTablePagination.tsx
@@ -182,11 +182,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) => (
@@ -360,9 +359,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
+ *
*
*
*
diff --git a/src/components/Dialog/DialogActions.tsx b/src/components/Dialog/DialogActions.tsx
index 7b34739da5..b4f2dc922b 100644
--- a/src/components/Dialog/DialogActions.tsx
+++ b/src/components/Dialog/DialogActions.tsx
@@ -35,8 +35,8 @@ export type Props = React.ComponentPropsWithRef & {
*
*
*
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
index 1c56b7b574..42576f386e 100644
--- a/src/components/Menu/Menu.tsx
+++ b/src/components/Menu/Menu.tsx
@@ -163,7 +163,7 @@ const isBrowser = () => Platform.OS === 'web' && 'document' in global;
*
* );
* };
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();
+ 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();
@@ -49,43 +58,45 @@ it('renders outlined button with mode', () => {
it('renders contained contained 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('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 +108,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 +122,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 +130,7 @@ it('renders button with color', () => {
it('renders button with button color', () => {
const tree = render(
- Custom Button
+
).toJSON();
expect(tree).toMatchSnapshot();
@@ -125,7 +138,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 +146,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 +157,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 +165,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 +184,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 +201,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 +217,7 @@ it('should execute onPressIn', () => {
const onPress = jest.fn();
const { getByTestId } = render(
-
- {null}
-
+
);
fireEvent(getByTestId('button'), 'onPressIn');
expect(onPressInMock).toHaveBeenCalledTimes(1);
@@ -213,20 +228,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 +287,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 +299,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,
@@ -264,9 +317,13 @@ describe('button icon styles', () => {
(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,
@@ -277,9 +334,12 @@ describe('button icon styles', () => {
it('should return correct icon styles for text button', () => {
const { getByTestId } = render(
-
- text button
-
+
);
expect(getByTestId('compact-button-icon-container')).toHaveStyle({
marginLeft: 12,
@@ -291,9 +351,12 @@ describe('button icon styles', () => {
(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,
@@ -303,6 +366,58 @@ describe('button icon styles', () => {
);
});
+describe('icon position', () => {
+ it('places the icon before the label by default', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-icon-container')).toHaveStyle({
+ marginLeft: 16,
+ marginRight: -16,
+ });
+ });
+
+ it('places the icon after the label when iconPosition is "trailing"', () => {
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('button-icon-container')).toHaveStyle({
+ marginLeft: -16,
+ 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: -16,
+ marginRight: 16,
+ });
+ expect(warn).toHaveBeenCalledWith(
+ expect.stringContaining('`contentStyle`')
+ );
+ warn.mockRestore();
+ });
+});
+
describe('getButtonColors - background color', () => {
const customButtonColor = '#111111';
@@ -698,6 +813,198 @@ describe('getButtonColors - border width', () => {
);
});
+describe('getButtonRippleColor', () => {
+ it('returns the custom ripple color when one is provided', () => {
+ expect(
+ getButtonRippleColor({ textColor: '#123456', customRippleColor: 'red' })
+ ).toBe('red');
+ });
+
+ it('defaults to the label color at the pressed-state opacity', () => {
+ expect(getButtonRippleColor({ textColor: '#123456' })).toBe(
+ color('#123456').alpha(stateOpacity.pressed).rgb().string()
+ );
+ });
+
+ it('returns undefined when the label color is not a plain string', () => {
+ expect(
+ getButtonRippleColor({ textColor: PlatformColor('?attr/colorPrimary') })
+ ).toBeUndefined();
+ });
+});
+
+describe('getButtonSizeStyle', () => {
+ it.each([
+ ['extra-small', 32, 12, 16, 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('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) => {
+ expect(getButtonShapeRadius({ size, shape: 'round' })).toBe(round);
+ expect(getButtonShapeRadius({ size, shape: 'square' })).toBe(square);
+ });
+
+ it('falls back to default radii when size is omitted', () => {
+ expect(getButtonShapeRadius({ shape: 'round' })).toBe(9999);
+ expect(getButtonShapeRadius({ shape: 'square' })).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,
+ textColor: 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,
+ textColor: getTheme().colors.onSecondaryContainer,
+ });
+ });
+
+ it('does not change contained colors when selected', () => {
+ expect(
+ getButtonColors({
+ theme: getTheme(),
+ mode: 'contained',
+ selected: true,
+ })
+ ).toMatchObject({
+ backgroundColor: getTheme().colors.primary,
+ textColor: getTheme().colors.onPrimary,
+ });
+ });
+});
+
it('animated value changes correctly', () => {
const value = new Animated.Value(1);
const { getByTestId } = render(
@@ -706,9 +1013,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 }],
diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx
index b11e006445..95cebf98ec 100644
--- a/src/components/__tests__/Card/Card.test.tsx
+++ b/src/components/__tests__/Card/Card.test.tsx
@@ -132,7 +132,7 @@ describe('CardActions', () => {
const { getByTestId } = render(
- Agree
+
);
@@ -150,9 +150,8 @@ describe('CardActions', () => {
testID="card-actions-button"
mode="contained"
style={styles.customBorderRadius}
- >
- Agree
-
+ label="Agree"
+ />
);
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 5737f4d313..e34f9af566 100644
--- a/src/components/__tests__/Menu.test.tsx
+++ b/src/components/__tests__/Menu.test.tsx
@@ -23,7 +23,7 @@ it('renders visible menu', () => {