diff --git a/docs/assets/screenshots/segmented-button.png b/docs/assets/screenshots/segmented-button.png
new file mode 100644
index 0000000000..bf7297a15a
Binary files /dev/null and b/docs/assets/screenshots/segmented-button.png differ
diff --git a/docs/assets/screenshots/segmentedbuttons.gif b/docs/assets/screenshots/segmentedbuttons.gif
new file mode 100644
index 0000000000..72bb80868d
Binary files /dev/null and b/docs/assets/screenshots/segmentedbuttons.gif differ
diff --git a/docs/pages/10.migration-guide-to-5.0.md b/docs/pages/10.migration-guide-to-5.0.md
index a4c6937674..da46723a25 100644
--- a/docs/pages/10.migration-guide-to-5.0.md
+++ b/docs/pages/10.migration-guide-to-5.0.md
@@ -491,6 +491,39 @@ Previously `elevation` was passed inside the `style` prop. Since it supported no
+
```
+## SegmentedButtons
+
+`SegmentedButtons` is a completely new component introduced in the latest version. It allows people to select options, switch views, or sort elements. It supports single and multiselect select variant and provide a lot of customization options.
+
+![segmentedButtons](screenshots/segmentedbuttons.gif)
+
+```js
+const MyComponent = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+ );
+};
+```
+
## TextInput.Icon
The property `name` was renamed to `icon`, since the scope and type of that prop is much wider than just the icon name – it accepts also the function which receives an object with color and size properties and
diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx
index 05c034b835..fb09009a45 100644
--- a/example/src/ExampleList.tsx
+++ b/example/src/ExampleList.tsx
@@ -38,6 +38,7 @@ import TouchableRippleExample from './Examples/TouchableRippleExample';
import ThemeExample from './Examples/ThemeExample';
import RadioButtonItemExample from './Examples/RadioButtonItemExample';
import AnimatedFABExample from './Examples/AnimatedFABExample';
+import SegmentedButtonExample from './Examples/SegmentedButtonsExample';
export const examples: Record<
string,
@@ -69,6 +70,7 @@ export const examples: Record<
radioGroup: RadioButtonGroupExample,
radioItem: RadioButtonItemExample,
searchbar: SearchbarExample,
+ segmentedButton: SegmentedButtonExample,
snackbar: SnackbarExample,
surface: SurfaceExample,
switch: SwitchExample,
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx
new file mode 100644
index 0000000000..794680985a
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonDefault.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonDefault = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ },
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonDefault;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx
new file mode 100644
index 0000000000..dc2cd99717
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonDisabled.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonDisabled = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ },
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonDisabled;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx
new file mode 100644
index 0000000000..5d5c17c094
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselect.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonMultiselect = () => {
+ const [value, setValue] = React.useState([]);
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+ button: {
+ flex: 1,
+ },
+});
+
+export default SegmentedButtonMultiselect;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx
new file mode 100644
index 0000000000..4ae2f9991a
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectIcons.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonMultiselectIcons = () => {
+ const [value, setValue] = React.useState([]);
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonMultiselectIcons;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx
new file mode 100644
index 0000000000..276b9ab406
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIcons.tsx
@@ -0,0 +1,37 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonOnlyIcons = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonOnlyIcons;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx
new file mode 100644
index 0000000000..7ba9fcfba3
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonOnlyIconsWithCheck.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonOnlyIconsWithCheck = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonOnlyIconsWithCheck;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx
new file mode 100644
index 0000000000..0012d14d64
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonWithDensity.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonWithDensity = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ },
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonWithDensity;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx
new file mode 100644
index 0000000000..ce2cc85463
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonWithSelectedCheck.tsx
@@ -0,0 +1,49 @@
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import { List, SegmentedButtons } from 'react-native-paper';
+
+const SegmentedButtonWithSelectedCheck = () => {
+ const [value, setValue] = React.useState('');
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ flex: 1,
+ },
+ group: { paddingHorizontal: 20, justifyContent: 'center' },
+});
+
+export default SegmentedButtonWithSelectedCheck;
diff --git a/example/src/Examples/SegmentedButtons/index.ts b/example/src/Examples/SegmentedButtons/index.ts
new file mode 100644
index 0000000000..d430fd3212
--- /dev/null
+++ b/example/src/Examples/SegmentedButtons/index.ts
@@ -0,0 +1,8 @@
+export { default as SegmentedButtonDefault } from './SegmentedButtonDefault';
+export { default as SegmentedButtonWithSelectedCheck } from './SegmentedButtonWithSelectedCheck';
+export { default as SegmentedButtonWithDensity } from './SegmentedButtonWithDensity';
+export { default as SegmentedButtonOnlyIcons } from './SegmentedButtonOnlyIcons';
+export { default as SegmentedButtonOnlyIconsWithCheck } from './SegmentedButtonOnlyIconsWithCheck';
+export { default as SegmentedButtonMultiselect } from './SegmentedButtonMultiselect';
+export { default as SegmentedButtonMultiselectIcons } from './SegmentedButtonMultiselectIcons';
+export { default as SegmentedButtonDisabled } from './SegmentedButtonDisabled';
diff --git a/example/src/Examples/SegmentedButtonsExample.tsx b/example/src/Examples/SegmentedButtonsExample.tsx
new file mode 100644
index 0000000000..76b26e7029
--- /dev/null
+++ b/example/src/Examples/SegmentedButtonsExample.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import ScreenWrapper from '../ScreenWrapper';
+import {
+ SegmentedButtonDefault,
+ SegmentedButtonDisabled,
+ SegmentedButtonMultiselect,
+ SegmentedButtonMultiselectIcons,
+ SegmentedButtonOnlyIcons,
+ SegmentedButtonOnlyIconsWithCheck,
+ SegmentedButtonWithDensity,
+ SegmentedButtonWithSelectedCheck,
+} from './SegmentedButtons';
+
+const SegmentedButtonExample = () => (
+
+
+
+
+
+
+
+
+
+
+);
+
+SegmentedButtonExample.title = 'Segmented Buttons';
+
+export default SegmentedButtonExample;
diff --git a/src/components/SegmentedButtons/SegmentedButtonItem.tsx b/src/components/SegmentedButtons/SegmentedButtonItem.tsx
new file mode 100644
index 0000000000..4a829ee1a1
--- /dev/null
+++ b/src/components/SegmentedButtons/SegmentedButtonItem.tsx
@@ -0,0 +1,222 @@
+import * as React from 'react';
+import {
+ StyleProp,
+ ViewStyle,
+ GestureResponderEvent,
+ StyleSheet,
+ View,
+ TextStyle,
+ Animated,
+} from 'react-native';
+import { useTheme } from '../../core/theming';
+import Text from '../Typography/Text';
+import TouchableRipple from '../TouchableRipple/TouchableRipple';
+import type { IconSource } from '../Icon';
+import color from 'color';
+import Icon from '../Icon';
+import {
+ getSegmentedButtonBorderRadius,
+ getSegmentedButtonColors,
+ getSegmentedButtonDensityPadding,
+} from './utils';
+
+export type Props = {
+ /**
+ * Whether the segmented button is checked
+ */
+ checked: boolean;
+ /**
+ * Icon to display for the `SegmentedButtonItem`.
+ */
+ icon?: IconSource;
+ /**
+ * Whether the button is disabled.
+ */
+ disabled?: boolean;
+ /**
+ * Accessibility label for the `SegmentedButtonItem`. This is read by the screen reader when the user taps the button.
+ */
+ accessibilityLabel?: string;
+ /**
+ * Function to execute on press.
+ */
+ onPress?: (event: GestureResponderEvent) => void;
+ /**
+ * Value of button.
+ */
+ value: string;
+ /**
+ * Label text of the button.
+ */
+ label?: string;
+ /**
+ * Button segment.
+ */
+ segment?: 'first' | 'last';
+ /**
+ * Show optional check icon to indicate selected state
+ */
+ showSelectedCheck?: boolean;
+ /**
+ * Density is applied to the height, to allow usage in denser UIs.
+ */
+ density?: 'regular' | 'small' | 'medium' | 'high';
+ style?: StyleProp;
+ /**
+ * testID to be used on tests.
+ */
+ testID?: string;
+};
+
+const SegmentedButtonItem = ({
+ checked,
+ accessibilityLabel,
+ disabled,
+ style,
+ showSelectedCheck,
+ icon,
+ testID,
+ label,
+ onPress,
+ segment,
+ density = 'regular',
+}: Props) => {
+ const theme = useTheme();
+
+ const checkScale = React.useRef(new Animated.Value(0)).current;
+
+ React.useEffect(() => {
+ if (!showSelectedCheck) {
+ return;
+ }
+ if (checked) {
+ Animated.spring(checkScale, {
+ toValue: 1,
+ useNativeDriver: true,
+ }).start();
+ } else {
+ Animated.spring(checkScale, {
+ toValue: 0,
+ useNativeDriver: true,
+ }).start();
+ }
+ }, [checked, checkScale, showSelectedCheck]);
+
+ const { roundness, isV3 } = theme;
+ const { borderColor, textColor, borderWidth, backgroundColor } =
+ getSegmentedButtonColors({
+ checked,
+ theme,
+ disabled,
+ });
+
+ const borderRadius = (isV3 ? 5 : 1) * roundness;
+ const segmentBorderRadius = getSegmentedButtonBorderRadius({
+ theme,
+ segment,
+ });
+ const rippleColor = color(textColor).alpha(0.12).rgb().string();
+
+ const iconSize = isV3 ? 18 : 16;
+ const iconStyle = {
+ marginRight: label ? 5 : checked && showSelectedCheck ? 3 : 0,
+ ...(label && {
+ transform: [
+ {
+ scale: checkScale.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1, 0],
+ }),
+ },
+ ],
+ }),
+ };
+
+ const buttonStyle: ViewStyle = {
+ backgroundColor,
+ borderColor,
+ borderWidth,
+ borderRadius,
+ ...segmentBorderRadius,
+ };
+ const paddingVertical = getSegmentedButtonDensityPadding({ density });
+ const rippleStyle: ViewStyle = {
+ borderRadius,
+ ...segmentBorderRadius,
+ };
+ const showIcon = icon && !label ? true : checked ? !showSelectedCheck : true;
+ const textStyle: TextStyle = {
+ ...(!isV3 && {
+ textTransform: 'uppercase',
+ fontWeight: '500',
+ }),
+ color: textColor,
+ };
+
+ return (
+
+
+
+ {checked && showSelectedCheck ? (
+
+
+
+ ) : null}
+ {showIcon ? (
+
+
+
+ ) : null}
+
+ {label}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ minWidth: 76,
+ borderStyle: 'solid',
+ },
+ label: {
+ textAlign: 'center',
+ },
+ content: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 9,
+ paddingHorizontal: 16,
+ },
+});
+
+export default SegmentedButtonItem;
+
+const SegmentedButtonWithTheme = SegmentedButtonItem;
+export { SegmentedButtonWithTheme as SegmentedButton };
diff --git a/src/components/SegmentedButtons/SegmentedButtons.tsx b/src/components/SegmentedButtons/SegmentedButtons.tsx
new file mode 100644
index 0000000000..c6fe2a2007
--- /dev/null
+++ b/src/components/SegmentedButtons/SegmentedButtons.tsx
@@ -0,0 +1,178 @@
+import * as React from 'react';
+import {
+ GestureResponderEvent,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+} from 'react-native';
+import { useTheme } from '../../core/theming';
+import SegmentedButtonItem from './SegmentedButtonItem';
+import { getDisabledSegmentedButtonStyle } from './utils';
+import type { IconSource } from '../Icon';
+
+type ConditionalValue =
+ | {
+ /**
+ * Array of the currently selected segmented button values.
+ */
+ value: string[];
+ /**
+ * Support multiple selected options.
+ */
+ multiSelect: true;
+ /**
+ * Function to execute on selection change
+ */
+ onValueChange: (value: string[]) => void;
+ }
+ | {
+ /**
+ * Value of the currently selected segmented button.
+ */
+ value: string;
+ /**
+ * Support multiple selected options.
+ */
+ multiSelect?: false;
+ /**
+ * Function to execute on selection change
+ */
+ onValueChange: (value: string) => void;
+ };
+
+export type Props = {
+ /**
+ * Buttons to display as options in toggle button.
+ * Button should contain the following properties:
+ * - `value`: value of button (required)
+ * - `icon`: icon to display for the item
+ * - `disabled`: whether the button is disabled
+ * - `accessibilityLabel`: acccessibility label for the button. This is read by the screen reader when the user taps the button.
+ * - `onPress`: callback that is called when button is pressed
+ * - `label`: label text of the button
+ * - `showSelectedCheck`: show optional check icon to indicate selected state
+ * - `style`: pass additional styles for the button
+ * - `testID`: testID to be used on tests
+ */
+ buttons: {
+ value: string;
+ icon?: IconSource;
+ disabled?: boolean;
+ accessibilityLabel?: string;
+ onPress?: (event: GestureResponderEvent) => void;
+ label?: string;
+ showSelectedCheck?: boolean;
+ style?: StyleProp;
+ testID?: string;
+ }[];
+ /**
+ * Density is applied to the height, to allow usage in denser UIs
+ */
+ density?: 'regular' | 'small' | 'medium' | 'high';
+ style?: StyleProp;
+} & ConditionalValue;
+
+/**
+ * @supported Available in v5.x
+ * Segmented buttons can be used to select options, switch views or sort elements.
+ *
+ *
+ *
+ *
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { SegmentedButtons } from 'react-native-paper';
+ *
+ * const MyComponent = () => {
+ * const [value, setValue] = React.useState('');
+ *
+ * return (
+ *
+ * );
+ * };
+ *
+ * export default MyComponent;
+ *```
+ */
+const SegmentedButtons = ({
+ value,
+ onValueChange,
+ buttons,
+ multiSelect,
+ density,
+ style,
+}: Props) => {
+ const theme = useTheme();
+ return (
+
+ {buttons.map((item, i) => {
+ const disabledChildStyle = getDisabledSegmentedButtonStyle({
+ theme,
+ buttons,
+ index: i,
+ });
+ const segment =
+ i === 0 ? 'first' : i === buttons.length - 1 ? 'last' : undefined;
+
+ const checked =
+ multiSelect && Array.isArray(value)
+ ? value.includes(item.value)
+ : value === item.value;
+
+ const onPress = (e: GestureResponderEvent) => {
+ item.onPress?.(e);
+
+ const nextValue =
+ multiSelect && Array.isArray(value)
+ ? checked
+ ? value.filter((val) => item.value !== val)
+ : [...value, item.value]
+ : item.value;
+
+ // @ts-expect-error: TS doesn't preserve types after destructuring, so the type isn't inferred correctly
+ onValueChange(nextValue);
+ };
+
+ return (
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ },
+});
+
+export default SegmentedButtons;
+
+// @component-docs ignore-next-line
+export { SegmentedButtons as SegmentedButtons };
diff --git a/src/components/SegmentedButtons/utils.ts b/src/components/SegmentedButtons/utils.ts
new file mode 100644
index 0000000000..d1ba1acd94
--- /dev/null
+++ b/src/components/SegmentedButtons/utils.ts
@@ -0,0 +1,157 @@
+import { StyleSheet, ViewStyle } from 'react-native';
+import color from 'color';
+import type { Theme } from '../../types';
+import { black, white } from '../../styles/themes/v2/colors';
+
+type BaseProps = {
+ theme: Theme;
+ disabled?: boolean;
+ checked: boolean;
+};
+
+const DEFAULT_PADDING = 9;
+
+export const getSegmentedButtonDensityPadding = ({
+ density,
+}: {
+ density?: 'regular' | 'small' | 'medium' | 'high';
+}) => {
+ let padding = DEFAULT_PADDING;
+
+ switch (density) {
+ case 'small':
+ return padding - 2;
+ case 'medium':
+ return padding - 4;
+ case 'high':
+ return padding - 8;
+ default:
+ return padding;
+ }
+};
+
+export const getDisabledSegmentedButtonStyle = ({
+ theme,
+ index,
+ buttons,
+}: {
+ theme: Theme;
+ buttons: { disabled?: boolean }[];
+ index: number;
+}): ViewStyle => {
+ const width = getSegmentedButtonBorderWidth({ theme });
+ const isDisabled = buttons[index]?.disabled;
+ const isNextDisabled = buttons[index + 1]?.disabled;
+
+ if (!isDisabled && isNextDisabled) {
+ return {
+ borderRightWidth: width,
+ };
+ }
+ return {};
+};
+
+export const getSegmentedButtonBorderRadius = ({
+ segment,
+ theme,
+}: {
+ theme: Theme;
+ segment?: 'first' | 'last';
+}): ViewStyle => {
+ if (segment === 'first') {
+ return {
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ ...(theme.isV3 && { borderRightWidth: 0 }),
+ };
+ } else if (segment === 'last') {
+ return {
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ };
+ } else {
+ return {
+ borderRadius: 0,
+ ...(theme.isV3 && { borderRightWidth: 0 }),
+ };
+ }
+};
+
+const getSegmentedButtonBackgroundColor = ({ checked, theme }: BaseProps) => {
+ if (checked) {
+ if (theme.isV3) {
+ return theme.colors.secondaryContainer;
+ } else {
+ return color(theme.colors.primary).alpha(0.12).rgb().string();
+ }
+ }
+ return 'transparent';
+};
+
+const getSegmentedButtonBorderColor = ({
+ theme,
+ disabled,
+ checked,
+}: BaseProps) => {
+ if (theme.isV3) {
+ if (disabled) {
+ return theme.colors.surfaceDisabled;
+ }
+ return theme.colors.outline;
+ }
+ if (checked) {
+ return theme.colors.primary;
+ }
+
+ return color(theme.dark ? white : black)
+ .alpha(0.29)
+ .rgb()
+ .string();
+};
+
+const getSegmentedButtonBorderWidth = ({
+ theme,
+}: Omit) => {
+ if (theme.isV3) {
+ return 1;
+ }
+
+ return StyleSheet.hairlineWidth;
+};
+
+const getSegmentedButtonTextColor = ({
+ theme,
+ disabled,
+}: Omit) => {
+ if (theme.isV3) {
+ if (disabled) {
+ return theme.colors.onSurfaceDisabled;
+ }
+ return theme.colors.onSurface;
+ } else {
+ if (disabled) {
+ return theme.colors.disabled;
+ }
+ return theme.colors.primary;
+ }
+};
+
+export const getSegmentedButtonColors = ({
+ theme,
+ disabled,
+ checked,
+}: BaseProps) => {
+ const backgroundColor = getSegmentedButtonBackgroundColor({
+ theme,
+ checked,
+ });
+ const borderColor = getSegmentedButtonBorderColor({
+ theme,
+ disabled,
+ checked,
+ });
+ const textColor = getSegmentedButtonTextColor({ theme, disabled });
+ const borderWidth = getSegmentedButtonBorderWidth({ theme });
+
+ return { backgroundColor, borderColor, textColor, borderWidth };
+};
diff --git a/src/components/__tests__/SegmentedButton.test.js b/src/components/__tests__/SegmentedButton.test.js
new file mode 100644
index 0000000000..8d1cf5d172
--- /dev/null
+++ b/src/components/__tests__/SegmentedButton.test.js
@@ -0,0 +1,281 @@
+import * as React from 'react';
+import renderer from 'react-test-renderer';
+import {
+ getDisabledSegmentedButtonStyle,
+ getSegmentedButtonColors,
+} from '../SegmentedButtons/utils';
+import { getTheme } from '../../core/theming';
+import { black } from '../../styles/themes/v2/colors';
+import color from 'color';
+import SegmentedButtons from '../SegmentedButtons/SegmentedButtons';
+import { render } from '@testing-library/react-native';
+
+it('renders segmented button', () => {
+ const tree = renderer
+ .create(
+ {}}
+ value={'walk'}
+ buttons={[{ value: 'walk' }, { value: 'ride' }]}
+ />
+ )
+ .toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders disabled segmented button', () => {
+ const tree = renderer
+ .create(
+ {}}
+ value={'walk'}
+ buttons={[{ value: 'walk' }, { value: 'ride', disabled: true }]}
+ />
+ )
+ .toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders checked segmented button with selected check', () => {
+ const tree = renderer
+ .create(
+ {}}
+ value={'walk'}
+ buttons={[
+ { value: 'walk', showSelectedCheck: true },
+ { value: 'ride', disabled: true },
+ ]}
+ />
+ )
+ .toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+describe('getSegmentedButtonColors', () => {
+ it('should return correct background color when checked and theme version 3', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(),
+ disabled: false,
+ checked: true,
+ })
+ ).toMatchObject({ backgroundColor: getTheme().colors.secondaryContainer });
+ });
+
+ it('should return correct background color when checked and theme version 2', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(false, false),
+ disabled: false,
+ checked: true,
+ })
+ ).toMatchObject({
+ backgroundColor: color(getTheme(false, false).colors.primary)
+ .alpha(0.12)
+ .rgb()
+ .string(),
+ });
+ });
+
+ it('should return correct background color when uncheked (V3 & V2)', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(false, false),
+ disabled: false,
+ checked: false,
+ })
+ ).toMatchObject({
+ backgroundColor: 'transparent',
+ });
+ });
+
+ it('should return correct border color with theme version 3', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(),
+ disabled: false,
+ checked: false,
+ })
+ ).toMatchObject({
+ borderColor: getTheme().colors.outline,
+ });
+ });
+
+ it('should return correct border color with theme version 2', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(false, false),
+ disabled: false,
+ checked: false,
+ })
+ ).toMatchObject({
+ borderColor: color(black).alpha(0.29).rgb().string(),
+ });
+ });
+
+ it('should return correct border color when disabled and theme version 3', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(),
+ disabled: true,
+ checked: false,
+ })
+ ).toMatchObject({
+ borderColor: getTheme().colors.surfaceDisabled,
+ });
+ });
+
+ it('should return correct textColor with theme version 3', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(),
+ disabled: false,
+ checked: false,
+ })
+ ).toMatchObject({
+ textColor: getTheme().colors.onSurface,
+ });
+ });
+
+ it('should return correct textColor with theme version 2', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(false, false),
+ disabled: false,
+ checked: false,
+ })
+ ).toMatchObject({
+ textColor: getTheme(false, false).colors.primary,
+ });
+ });
+
+ it('should return correct textColor when disabled and theme version 3', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(),
+ disabled: true,
+ checked: false,
+ })
+ ).toMatchObject({
+ textColor: getTheme().colors.onSurfaceDisabled,
+ });
+ });
+
+ it('should return correct textColor when disabled and theme version 2', () => {
+ expect(
+ getSegmentedButtonColors({
+ theme: getTheme(false, false),
+ disabled: true,
+ checked: false,
+ })
+ ).toMatchObject({
+ textColor: getTheme(false, false).colors.disabled,
+ });
+ });
+});
+
+describe('getDisabledSegmentedButtonBorderWidth', () => {
+ it('Returns empty style object for all enabled buttons', () => {
+ [0, 1, 2].forEach((index) => {
+ expect(
+ getDisabledSegmentedButtonStyle({
+ theme: getTheme(),
+ buttons: [
+ { disabled: false },
+ { disabled: false },
+ { disabled: false },
+ ],
+ index,
+ })
+ ).toMatchObject({});
+ });
+ });
+
+ it('Returns empty style object for all disabled buttons', () => {
+ [0, 1, 2].forEach((index) => {
+ expect(
+ getDisabledSegmentedButtonStyle({
+ theme: getTheme(),
+ buttons: [{ disabled: true }, { disabled: true }, { disabled: true }],
+ index,
+ })
+ ).toMatchObject({});
+ });
+ });
+
+ it('Returns proper style object for one disabled button', () => {
+ expect(
+ getDisabledSegmentedButtonStyle({
+ theme: getTheme(),
+ buttons: [{ disabled: false }, { disabled: true }, { disabled: true }],
+ index: 0,
+ })
+ ).toMatchObject({ borderRightWidth: 1 });
+ });
+
+ it('Returns proper style object for two disabled buttons (alternately)', () => {
+ [0, 2].forEach((index) => {
+ expect(
+ getDisabledSegmentedButtonStyle({
+ theme: getTheme(),
+ buttons: [
+ { disabled: false },
+ { disabled: true },
+ { disabled: false },
+ { disabled: true },
+ ],
+ index,
+ })
+ ).toMatchObject({ borderRightWidth: 1 });
+ });
+ });
+});
+
+describe('should have `accessibilityState={ checked: true }` when selected', () => {
+ it('should have two button selected', () => {
+ const onValueChange = jest.fn();
+ const { getAllByA11yState } = render(
+
+ );
+
+ const checkedButtons = getAllByA11yState({ checked: true });
+ expect(checkedButtons).toHaveLength(2);
+ });
+
+ it('show selected check icon should be shown', () => {
+ const onValueChange = jest.fn();
+
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('walking-check-icon')).toBeDefined();
+ });
+});
diff --git a/src/components/__tests__/__snapshots__/SegmentedButton.test.js.snap b/src/components/__tests__/__snapshots__/SegmentedButton.test.js.snap
new file mode 100644
index 0000000000..9e82770d43
--- /dev/null
+++ b/src/components/__tests__/__snapshots__/SegmentedButton.test.js.snap
@@ -0,0 +1,727 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders checked segmented button with selected check 1`] = `
+
+
+
+
+
+
+ □
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders disabled segmented button 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders segmented button 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/index.tsx b/src/index.tsx
index bb0ad65ee2..c1b4962811 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -53,6 +53,7 @@ export { default as Appbar } from './components/Appbar';
export { default as TouchableRipple } from './components/TouchableRipple/TouchableRipple';
export { default as TextInput } from './components/TextInput/TextInput';
export { default as ToggleButton } from './components/ToggleButton';
+export { default as SegmentedButtons } from './components/SegmentedButtons/SegmentedButtons';
export {
Caption,
@@ -143,6 +144,7 @@ export type { Props as ParagraphProps } from './components/Typography/v2/Paragra
export type { Props as SubheadingProps } from './components/Typography/v2/Subheading';
export type { Props as TitleProps } from './components/Typography/v2/Title';
export type { Props as TextProps } from './components/Typography/Text';
+export type { Props as SegmentedButtonsProps } from './components/SegmentedButtons/SegmentedButtons';
export type {
MD2Theme,