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
-
+
```
:::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 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
-
+
);
}
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
+ 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 = () => {
>
-
-
-
+ }>
+ * 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__/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', () => {
}
+ anchor={}
>
@@ -40,7 +40,7 @@ it('renders not visible menu', () => {
}
+ anchor={}
>
@@ -57,7 +57,7 @@ it('renders menu with content styles', () => {
}
+ anchor={}
contentStyle={styles.contentStyle}
>
@@ -78,7 +78,7 @@ it('renders menu with content styles', () => {
}
+ anchor={}
elevation={elevation}
>
@@ -110,11 +110,7 @@ it('uses the default anchorPosition of top', async () => {
- }
+ anchor={}
contentStyle={styles.contentStyle}
>
@@ -166,11 +162,7 @@ it('respects anchorPosition bottom', async () => {
- }
+ anchor={}
anchorPosition="bottom"
contentStyle={styles.contentStyle}
>
@@ -209,7 +201,7 @@ it('animated value changes correctly', () => {
}
+ anchor={}
testID="menu"
contentStyle={[{ transform: [{ scale: value }] }]}
>
@@ -241,7 +233,7 @@ it('renders menu with mode "elevated"', () => {
}
+ anchor={}
mode="elevated"
>
@@ -265,7 +257,7 @@ it('renders menu with mode "flat"', () => {
`;
-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,
},