diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md
index a4d7123a09..07afe67172 100644
--- a/docs/6.x/docs/guides/migration.md
+++ b/docs/6.x/docs/guides/migration.md
@@ -80,6 +80,28 @@ You can specify a `testID` explicitly and use that value to query the component.
## Components
+### Chip
+
+#### Default mode
+
+- The default **`mode`** changed from **`"flat"`** to **`"outlined"`**.
+
+```tsx
+// Before (v5) - no mode passed, rendered flat
+ {}}>Example Chip
+
+// After (v6) - no mode passed, renders outlined; pass mode="flat" to keep the old look
+ {}}>Example Chip
+```
+
+#### Removed props
+
+- **`compact`**, **`showSelectedOverlay`** were removed and have no direct equivalent.
+
+#### New props
+
+- **`trailingIcon`** / **`onTrailingIconPress`** / **`trailingIconAccessibilityLabel`** add a trailing action (e.g. a dropdown) independent of the close button. `trailingIcon` takes precedence over `onClose`'s close icon when both are specified.
+
### Appbar
The `style` props for `Appbar` and `Appbar.Header` no longer accept `Animated.Value` or `Animated.AnimatedInterpolation`. They only accept static styles.
diff --git a/docs/public/gallery/chip-dark.png b/docs/public/gallery/chip-dark.png
index ac6ea638fe..88eb515eb8 100644
Binary files a/docs/public/gallery/chip-dark.png and b/docs/public/gallery/chip-dark.png differ
diff --git a/docs/public/gallery/chip.png b/docs/public/gallery/chip.png
index 6613a45bb5..aaf56dbb1a 100644
Binary files a/docs/public/gallery/chip.png and b/docs/public/gallery/chip.png differ
diff --git a/docs/public/screenshots/chip.png b/docs/public/screenshots/chip.png
new file mode 100644
index 0000000000..aaf56dbb1a
Binary files /dev/null and b/docs/public/screenshots/chip.png differ
diff --git a/docs/src/data/screenshots.ts b/docs/src/data/screenshots.ts
index 92bf8f2788..b2ff7f99e0 100644
--- a/docs/src/data/screenshots.ts
+++ b/docs/src/data/screenshots.ts
@@ -51,10 +51,7 @@ export const screenshots = {
enabled: 'screenshots/checkbox-enabled.ios.png',
disabled: 'screenshots/checkbox-disabled.ios.png',
},
- Chip: {
- flat: 'screenshots/chip-1.png',
- outlined: 'screenshots/chip-2.png',
- },
+ Chip: 'screenshots/chip.png',
DataTable: 'screenshots/data-table-full-width.png',
'DataTable.Cell': 'screenshots/data-table-row-cell.png',
'DataTable.Header': 'screenshots/data-table-header.png',
diff --git a/example/src/Examples/ChipExample.tsx b/example/src/Examples/ChipExample.tsx
index c6c817041f..9279c1a5d1 100644
--- a/example/src/Examples/ChipExample.tsx
+++ b/example/src/Examples/ChipExample.tsx
@@ -2,11 +2,14 @@ import * as React from 'react';
import { Image, StyleSheet, View } from 'react-native';
import color from 'color';
-import { Chip, List, Palette, Snackbar, Text } from 'react-native-paper';
+import { Chip, List, Palette, Snackbar } from 'react-native-paper';
import ScreenWrapper from '../ScreenWrapper';
+const filters = ['All', 'Unread', 'Starred'];
+
const ChipExample = () => {
+ const [selectedFilter, setSelectedFilter] = React.useState(filters[0]);
const [snackbarProperties, setSnackbarProperties] = React.useState({
visible: false,
text: '',
@@ -16,168 +19,81 @@ const ChipExample = () => {
return (
<>
-
+
- {}} style={styles.chip}>
- Simple
+ {filters.map((filter) => (
+ setSelectedFilter(filter)}
+ style={styles.chip}
+ >
+ {filter}
+
+ ))}
+ {}} style={styles.chip}>
+ With icon
{}}
style={styles.chip}
>
- With selected overlay
-
- {}} style={styles.chip}>
- Elevated
+ No check
- {}}>
- Compact chip
-
- {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Close button pressed',
- })
- }
- style={styles.chip}
- closeIconAccessibilityLabel="Close icon accessibility label"
- >
- Close button
-
- {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Heart icon close button pressed',
- })
- }
- style={styles.chip}
- >
- Icon
-
-
- }
- onPress={() => {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Avatar close button pressed',
- })
- }
- style={styles.chip}
- >
- Avatar
+
+ Disabled
- }
+ trailingIcon="menu-down"
onPress={() => {}}
- style={styles.chip}
- >
- Avatar (selected)
-
-
+ onTrailingIconPress={() =>
setSnackbarProperties({
visible: true,
- text: 'Disabled heart icon close button pressed',
+ text: 'Filter menu icon pressed',
})
}
+ trailingIconAccessibilityLabel="Open filter menu"
style={styles.chip}
>
- Icon (disabled)
-
-
- }
- style={styles.chip}
- >
- Avatar (disabled)
+ Trailing icon
-
+
+
- {}} style={styles.chip}>
- Simple
+ {}} style={styles.chip}>
+ Outlined
{}}
style={styles.chip}
>
- With selected overlay
+ Flat
{}}
style={styles.chip}
>
Elevated
- {}}
- style={styles.chip}
- >
- Compact chip
-
- {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Close button pressed',
- })
- }
- style={styles.chip}
- >
- Close button
-
- {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Heart icon close button pressed',
- })
- }
- style={styles.chip}
- >
- Icon
+ {}} style={styles.chip}>
+ No icon
+
+
+
+
+
{
{
onPress={() => {}}
style={styles.chip}
>
- Avatar (selected)
+ Selected avatar
{}}
onClose={() =>
setSnackbarProperties({
visible: true,
- text: 'Disabled close button pressed',
+ text: 'Close button pressed',
})
}
style={styles.chip}
>
- Icon (disabled)
+ Removable
+ closeIcon="arrow-down"
+ onPress={() => {}}
+ onClose={() =>
+ setSnackbarProperties({
+ visible: true,
+ text: 'Custom trailing icon pressed',
+ })
}
style={styles.chip}
+ closeIconAccessibilityLabel="Custom trailing icon accessibility label"
>
- Avatar (disabled)
+ Custom trailing
-
+
+
- {}}
- compact
- avatar={
-
- }
- style={styles.chip}
- theme={{ shapes: { corner: { small: 16 } } }}
- >
- Compact with custom border radius
+ {}} style={styles.chip}>
+ Outlined
- {}}
- compact
- avatar={
-
- }
- style={styles.chip}
- theme={{ shapes: { corner: { small: 16 } } }}
- >
- Compact with custom border radius
+ {}} style={styles.chip}>
+ Flat
- {}}
- onLongPress={() =>
- setSnackbarProperties({ visible: true, text: '' })
- }
- style={styles.chip}
- >
- With onLongPress
+ {}} style={styles.chip}>
+ Elevated
- {}}
- style={styles.chip}
- theme={{
- colors: {
- secondaryContainer: color(customColor)
- .alpha(0.2)
- .rgb()
- .string(),
- },
- }}
- selectedColor={customColor}
- >
- Flat selected chip with custom color
-
- {}}
- style={styles.chip}
- selectedColor={customColor}
- >
- Flat unselected chip with custom color
+ {}} style={styles.chip}>
+ With icon
+
+
+
+
+
{}}
- style={styles.chip}
- theme={{
- colors: {
- surface: color(customColor).alpha(0.2).rgb().string(),
- },
- }}
- selectedColor={customColor}
- >
- Outlined selected chip with custom color
-
- {}}
- style={styles.chip}
selectedColor={customColor}
+ style={[
+ styles.chip,
+ {
+ backgroundColor: color(customColor).alpha(0.2).rgb().string(),
+ },
+ ]}
>
- Outlined unselected chip with custom color
-
- {}}
- style={styles.chip}
- textStyle={styles.tiny}
- >
- With custom size
-
- {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Close button pressed',
- })
- }
- style={styles.bigTextFlex}
- textStyle={styles.bigTextStyle}
- ellipsizeMode="middle"
- >
- With a very big text: React Native Paper is a high-quality,
- standard-compliant Material Design library that has you covered in
- all major use-cases.
+ Custom color
{}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Custom icon close button pressed',
- })
- }
- closeIcon="arrow-down"
- style={styles.chip}
- closeIconAccessibilityLabel="Custom Close icon accessibility label"
+ style={[styles.chip, styles.customBorderRadius]}
>
- With custom close icon
+ Rounded
- {}}
- style={styles.chip}
- textStyle={styles.tiny}
- >
- With custom text
+ {}} style={styles.fullWidthChip}>
+ Full width chip
- {}} style={styles.fullWidthChip}>
- Full width chip
-
({
onChange(option)}
>
{option}
diff --git a/example/src/Examples/TeamDetails.tsx b/example/src/Examples/TeamDetails.tsx
index 5970274f31..59c5d2c63e 100644
--- a/example/src/Examples/TeamDetails.tsx
+++ b/example/src/Examples/TeamDetails.tsx
@@ -57,12 +57,7 @@ const News = () => {
style={styles.chipsContainer}
contentContainerStyle={styles.chipsContent}
>
- {}}
- style={styles.chip}
- showSelectedOverlay
- >
+ {}} style={styles.chip}>
Latest
{}} style={styles.chip}>
diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx
index 3bbbe32cf5..17fef4d53a 100644
--- a/src/components/Chip/Chip.tsx
+++ b/src/components/Chip/Chip.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Platform, StyleSheet, Pressable, View } from 'react-native';
+import { Platform, StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
@@ -7,30 +7,47 @@ import type {
StyleProp,
TextStyle,
ViewProps,
+ ViewStyle,
} from 'react-native';
-import useLatestCallback from 'use-latest-callback';
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+} from 'react-native-reanimated';
-import { getChipColors } from './helpers';
-import type { ChipAvatarProps } from './helpers';
+import { ChipTokens } from './tokens';
+import { getChipColors } from './utils';
+import type { ChipAvatarProps } from './utils';
import { useInternalTheme } from '../../core/theming';
-import { white } from '../../theme/colors';
import type { EllipsizeProp, ThemeProp } from '../../types';
import hasTouchHandler from '../../utils/hasTouchHandler';
+import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent';
import type { IconSource } from '../Icon';
import Icon from '../Icon';
-import MaterialCommunityIcon from '../MaterialCommunityIcon';
import Surface from '../Surface';
-import type { SurfaceStyle } from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
+// The trailing icon's ripple is a square that stretches flush to the chip's
+// top/bottom edges, so it's sized to match the chip's height rather than a
+// fixed value of its own.
+const TRAILING_ICON_AREA_SIZE = ChipTokens.containerHeight;
+
+// The icon glyph is centered inside that (larger) ripple square, so its own
+// edge sits this far in from the ripple square's edge.
+const TRAILING_ICON_INSET = (TRAILING_ICON_AREA_SIZE - ChipTokens.iconSize) / 2;
+
+// Suppresses the browser's native focus outline so only our own focus
+// indicator ring is visible. Not in StyleSheet because `outline` is outside ViewStyle.
+// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+const webNoOutline = { outline: 'none' } as unknown as ViewStyle;
+
export type Props = Omit & {
/**
* Mode of the chip.
- * - `flat` - flat chip without outline.
- * - `outlined` - chip with an outline.
+ * - `flat` - chip with a filled container.
+ * - `outlined` - chip with an outline when unselected.
*/
mode?: 'flat' | 'outlined';
/**
@@ -38,15 +55,29 @@ export type Props = Omit & {
*/
children: React.ReactNode;
/**
- * Icon to display for the `Chip`. Both icon and avatar cannot be specified.
+ * Leading icon to display for the `Chip`. Takes precedence over `avatar` when both are specified.
*/
icon?: IconSource;
/**
- * Avatar to display for the `Chip`. Both icon and avatar cannot be specified.
+ * Leading avatar to display for the `Chip`. Ignored when `icon` is also specified.
*/
avatar?: React.ReactNode;
/**
- * Icon to display as the close button for the `Chip`. The icon appears only when the onClose prop is specified.
+ * Trailing icon to display for the `Chip`, independent of the close button (e.g. a dropdown or info icon).
+ * Takes precedence over `onClose`'s close icon when both are specified.
+ */
+ trailingIcon?: IconSource;
+ /**
+ * Function to execute when the trailing icon is pressed. Only called when `trailingIcon` is specified.
+ */
+ onTrailingIconPress?: (e: GestureResponderEvent) => void;
+ /**
+ * Accessibility label for the trailing icon. This is read by the screen reader when the user taps the trailing icon.
+ */
+ trailingIconAccessibilityLabel?: string;
+ /**
+ * Icon to display as the close button for the `Chip`, rendered in the trailing icon slot. The icon appears only
+ * when `onClose` is specified and `trailingIcon` is not.
*/
closeIcon?: IconSource;
/**
@@ -54,16 +85,10 @@ export type Props = Omit & {
*/
selected?: boolean;
/**
- * Whether to style the chip color as selected.
- * Note: With theme version 3 `selectedColor` doesn't apply to the `icon`.
- * If you want specify custom color for the `icon`, render your own `Icon` component.
+ * Custom color to use for the label, leading icon, trailing icon, and outlined border,
+ * overriding the default selected/unselected theme colors.
*/
selectedColor?: ColorValue;
- /**
- * @supported Available in v5.x with theme version 3
- * Whether to display overlay on selected chip
- */
- showSelectedOverlay?: boolean;
/**
* Whether to display default check icon on selected chip.
* Note: Check will not be shown if `icon` is specified. If specified, `icon` will be shown regardless of `selected`.
@@ -74,7 +99,7 @@ export type Props = Omit & {
*/
disabled?: boolean;
/**
- * Type of background drawabale to display the feedback (Android).
+ * Type of background drawable to display the feedback (Android).
* https://reactnative.dev/docs/pressable#rippleconfig
*/
background?: PressableAndroidRippleConfig;
@@ -84,6 +109,7 @@ export type Props = Omit & {
'aria-label'?: string;
/**
* Accessibility label for the close icon. This is read by the screen reader when the user taps the close icon.
+ * Ignored when `trailingIcon` is also specified — use `trailingIconAccessibilityLabel` instead.
*/
closeIconAccessibilityLabel?: string;
/**
@@ -93,7 +119,7 @@ export type Props = Omit & {
/**
* Function to execute on long press.
*/
- onLongPress?: () => void;
+ onLongPress?: (e: GestureResponderEvent) => void;
/**
* Function to execute as soon as the touchable element is pressed and invoked even before onPress.
*/
@@ -103,7 +129,8 @@ export type Props = Omit & {
*/
onPressOut?: (e: GestureResponderEvent) => void;
/**
- * Function to execute on close button press. The close button appears only when this prop is specified.
+ * Function to execute on close button press, indicating the chip should be removed. The close button appears
+ * only when this prop is specified and `trailingIcon` is not.
*/
onClose?: () => void;
/**
@@ -111,20 +138,14 @@ export type Props = Omit & {
*/
delayLongPress?: number;
/**
- * @supported Available in v5.x with theme version 3
- * Sets smaller horizontal paddings `12dp` around label, when there is only label.
- */
- compact?: boolean;
- /**
- * @supported Available in v5.x with theme version 3
- * Whether chip should have the elevation.
+ * Whether the chip should have elevation.
*/
elevated?: boolean;
/**
- * Style of chip's text
+ * Style of chip's text.
*/
textStyle?: StyleProp;
- style?: StyleProp;
+ style?: StyleProp;
/**
* Sets additional distance outside of element in which a press can be detected.
*/
@@ -138,7 +159,7 @@ export type Props = Omit & {
*/
testID?: string;
/**
- * Ellipsize Mode for the children text
+ * Ellipsize Mode for the label text.
*/
ellipsizeMode?: EllipsizeProp;
/**
@@ -153,7 +174,7 @@ export type Props = Omit & {
/**
* Chips are compact elements that can represent inputs, attributes, or actions.
- * They can have an icon or avatar on the left, and a close button icon on the right.
+ * They can have a leading icon or avatar, and a trailing icon (a close button or a custom action) on the right.
* They are typically used to:
*
* - Present multiple options
@@ -175,7 +196,7 @@ export type Props = Omit & {
* ```
*/
const Chip = ({
- mode = 'flat',
+ mode = 'outlined',
children,
icon,
avatar,
@@ -192,6 +213,9 @@ const Chip = ({
delayLongPress,
onClose,
closeIcon,
+ trailingIcon,
+ onTrailingIconPress,
+ trailingIconAccessibilityLabel = 'Trailing icon',
textStyle,
style,
theme: themeOverrides,
@@ -199,16 +223,13 @@ const Chip = ({
selectedColor,
showSelectedCheck = true,
ellipsizeMode,
- compact,
elevated = false,
maxFontSizeMultiplier,
hitSlop,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
-
- const [pressed, setPressed] = React.useState(false);
- const elevation = elevated ? (pressed ? 2 : 1) : 0;
+ const isOutlined = mode === 'outlined';
const hasPassedTouchHandler = hasTouchHandler({
onPress,
@@ -216,69 +237,106 @@ const Chip = ({
onPressIn,
onPressOut,
});
+ const isTouchableDisabled = disabled || !hasPassedTouchHandler;
- const isOutlined = mode === 'outlined';
-
- const handlePressIn = useLatestCallback((e: GestureResponderEvent) => {
- onPressIn?.(e);
- setPressed(true);
- });
-
- const handlePressOut = useLatestCallback((e: GestureResponderEvent) => {
- onPressOut?.(e);
- setPressed(false);
- });
+ const [hovered, setHovered] = React.useState(false);
+ const [focused, setFocused] = React.useState(false);
+ const focusedSV = useSharedValue(0);
+ const focusRingStyle = useAnimatedStyle(
+ () => ({ opacity: focusedSV.value }),
+ []
+ );
- const elevationTransitionDuration =
- theme.motion.duration[pressed ? 'short4' : 'short3'] *
- theme.animation.scale;
+ const trailingIconFocusedSV = useSharedValue(0);
+ const trailingIconFocusRingStyle = useAnimatedStyle(
+ () => ({ opacity: trailingIconFocusedSV.value }),
+ []
+ );
- const opacity = 0.38;
const defaultBorderRadius = theme.shapes.corner.small;
- const iconSize = 18;
-
- const borderRadius = defaultBorderRadius;
+ const {
+ backgroundColor: customBackgroundColor,
+ borderRadius = defaultBorderRadius,
+ } = StyleSheet.flatten(style) || {};
+ const focusRingBorderRadius =
+ typeof borderRadius === 'number'
+ ? borderRadius + ChipTokens.focusIndicatorOffset
+ : borderRadius;
const {
borderColor,
textColor,
iconColor,
+ trailingIconColor,
contentOpacity,
selectedBackgroundColor,
backgroundColor,
+ rippleColor,
+ avatarOverlayColor,
} = getChipColors({
isOutlined,
+ selected,
+ elevated,
+ focused,
theme,
selectedColor,
+ customBackgroundColor,
disabled,
});
- const multiplier = compact ? 1.5 : 2;
+ const hasAvatar = !!avatar && !icon;
+ const showSelectedIcon = selected && showSelectedCheck && !icon;
+ const showLeadingIcon = !!icon || showSelectedIcon;
+ const hasLeading = hasAvatar || showLeadingIcon;
+ const hasTrailingIcon = !!trailingIcon;
+ const hasClose = !hasTrailingIcon && !!onClose;
+ const showTrailingIcon = hasTrailingIcon || hasClose;
- const labelSpacings = {
- marginRight: onClose ? 0 : 8 * multiplier,
- marginLeft:
- avatar || icon || (selected && showSelectedCheck)
- ? 4 * multiplier
- : 8 * multiplier,
+ const leftPadding = hasAvatar
+ ? ChipTokens.avatarLeadingPadding
+ : hasLeading
+ ? ChipTokens.iconLeadingPadding
+ : ChipTokens.leadingPadding;
+ const rightPadding = showTrailingIcon
+ ? ChipTokens.trailingIconPadding + ChipTokens.iconSize + TRAILING_ICON_INSET
+ : ChipTokens.trailingPadding;
+ const touchTargetInset =
+ (ChipTokens.minimumTouchTarget - ChipTokens.containerHeight) / 2;
+ const touchTargetHitSlop = {
+ top: touchTargetInset,
+ bottom: touchTargetInset,
};
-
- const contentSpacings = {
- paddingRight: onClose ? 34 : 0,
- };
-
- const labelTextStyle = {
- color: textColor,
- ...theme.fonts.labelLarge,
+ const trailingIconTouchTargetInset =
+ (ChipTokens.minimumTouchTarget - TRAILING_ICON_AREA_SIZE) / 2;
+ const trailingIconHitSlop = {
+ top: trailingIconTouchTargetInset,
+ bottom: trailingIconTouchTargetInset,
+ left: trailingIconTouchTargetInset,
+ right: trailingIconTouchTargetInset,
};
+ const trailingIconAndroidRipple =
+ Platform.OS === 'android'
+ ? {
+ color: rippleColor,
+ borderless: true,
+ radius: TRAILING_ICON_AREA_SIZE / 2,
+ }
+ : undefined;
return (
setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ onFocus={(e) => {
+ if (!isKeyboardFocusEvent(e)) return;
+ focusedSV.value = 1;
+ setFocused(true);
+ }}
+ onBlur={() => {
+ focusedSV.value = 0;
+ setFocused(false);
+ }}
delayLongPress={delayLongPress}
- disabled={disabled}
+ disabled={isTouchableDisabled}
aria-label={ariaLabel}
role={role}
aria-selected={selected}
- aria-disabled={disabled}
+ aria-disabled={isTouchableDisabled}
testID={testID}
theme={theme}
- hitSlop={hitSlop}
+ hitSlop={hitSlop ?? touchTargetHitSlop}
>
- {avatar && !icon ? (
-
+ {hasAvatar ? (
+
{React.isValidElement(avatar)
? React.cloneElement(avatar, {
style: [styles.avatar, avatar.props.style],
})
: avatar}
+ {showSelectedIcon ? (
+
+
+
+ ) : null}
) : null}
- {icon || (selected && showSelectedCheck) ? (
-
- {icon ? (
-
- ) : (
-
- )}
+ {showLeadingIcon && !hasAvatar ? (
+
+
) : null}
@@ -378,101 +431,151 @@ const Chip = ({
- {onClose ? (
-
-
-
- {closeIcon ? (
-
- ) : (
-
- )}
-
-
-
+ {showTrailingIcon ? (
+ {
+ if (!isKeyboardFocusEvent(e)) return;
+ trailingIconFocusedSV.value = 1;
+ }}
+ onBlur={() => {
+ trailingIconFocusedSV.value = 0;
+ }}
+ style={[
+ styles.closeButton,
+ Platform.OS === 'web' ? webNoOutline : undefined,
+ disabled ? { opacity: contentOpacity } : null,
+ ]}
+ theme={theme}
+ >
+
+
+ ) : null}
+ {showTrailingIcon ? (
+
) : null}
+
);
};
const styles = StyleSheet.create({
container: {
- borderWidth: StyleSheet.hairlineWidth,
+ minHeight: ChipTokens.containerHeight,
+ borderWidth: ChipTokens.outlineWidth,
borderStyle: 'solid',
- flexDirection: Platform.select({ default: 'column', web: 'row' }),
+ justifyContent: 'center',
+ alignSelf: 'flex-start',
},
- md3Container: {
- borderWidth: 1,
+ touchable: {
+ minHeight: ChipTokens.containerHeight,
+ flexShrink: 1,
+ alignSelf: 'stretch',
+ justifyContent: 'center',
+ },
+ focusRing: {
+ position: 'absolute',
+ top: -ChipTokens.focusIndicatorOffset,
+ left: -ChipTokens.focusIndicatorOffset,
+ right: -ChipTokens.focusIndicatorOffset,
+ bottom: -ChipTokens.focusIndicatorOffset,
+ borderWidth: ChipTokens.focusIndicatorThickness,
+ pointerEvents: 'none',
},
content: {
+ flexShrink: 1,
flexDirection: 'row',
alignItems: 'center',
- paddingLeft: 4,
position: 'relative',
+ overflow: 'hidden',
},
- md3Content: {
- paddingLeft: 0,
- },
- icon: {
- padding: 4,
- alignSelf: 'center',
- },
- md3Icon: {
- paddingLeft: 8,
- paddingRight: 0,
- },
- closeIcon: {
- marginRight: 4,
- },
- md3CloseIcon: {
- marginRight: 8,
- padding: 0,
- },
- md3LabelText: {
- textAlignVertical: 'center',
- marginVertical: 6,
+ avatarWrapper: {
+ width: ChipTokens.avatarSize,
+ height: ChipTokens.avatarSize,
+ borderRadius: ChipTokens.avatarSize / 2,
+ marginRight: ChipTokens.leadingLabelGap,
+ overflow: 'hidden',
},
avatar: {
- width: 24,
- height: 24,
- borderRadius: 12,
- },
- avatarWrapper: {
- marginRight: 4,
+ width: ChipTokens.avatarSize,
+ height: ChipTokens.avatarSize,
+ borderRadius: ChipTokens.avatarSize / 2,
},
- md3AvatarWrapper: {
- marginLeft: 4,
- marginRight: 0,
+ avatarSelectedOverlay: {
+ ...StyleSheet.absoluteFill,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- md3SelectedIcon: {
- paddingLeft: 4,
+ leadingIcon: {
+ width: ChipTokens.iconSize,
+ height: ChipTokens.iconSize,
+ marginRight: ChipTokens.leadingLabelGap,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- // eslint-disable-next-line react-native/no-color-literals
- avatarSelected: {
- position: 'absolute',
- top: 4,
- left: 4,
- backgroundColor: 'rgba(0, 0, 0, .29)',
+ labelText: {
+ textAlignVertical: 'center',
+ includeFontPadding: false,
},
- closeButtonStyle: {
+ closeButton: {
position: 'absolute',
+ top: 0,
+ bottom: 0,
right: 0,
- height: '100%',
- justifyContent: 'center',
+ width: TRAILING_ICON_AREA_SIZE,
+ borderRadius: TRAILING_ICON_AREA_SIZE / 2,
+ overflow: 'hidden',
alignItems: 'center',
+ justifyContent: 'center',
},
- touchable: {
- width: '100%',
+ trailingIconFocusRing: {
+ position: 'absolute',
+ top: -ChipTokens.focusIndicatorOffset,
+ bottom: -ChipTokens.focusIndicatorOffset,
+ right: -ChipTokens.focusIndicatorOffset,
+ width: TRAILING_ICON_AREA_SIZE + ChipTokens.focusIndicatorOffset * 2,
+ borderRadius: TRAILING_ICON_AREA_SIZE / 2 + ChipTokens.focusIndicatorOffset,
+ borderWidth: ChipTokens.focusIndicatorThickness,
+ pointerEvents: 'none',
+ },
+ disabled: {
+ opacity: ChipTokens.disabledContentOpacity,
},
});
diff --git a/src/components/Chip/helpers.tsx b/src/components/Chip/helpers.tsx
deleted file mode 100644
index 1990bc59a7..0000000000
--- a/src/components/Chip/helpers.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
-
-import color from 'color';
-
-import { tokens } from '../../theme/tokens';
-import type { InternalTheme } from '../../types';
-
-const md3 = (theme: InternalTheme) => theme;
-
-const stateOpacity = tokens.md.sys.state.opacity;
-
-export type ChipAvatarProps = {
- style?: StyleProp;
-};
-
-type BaseProps = {
- theme: InternalTheme;
- isOutlined: boolean;
- disabled?: boolean;
-};
-
-const getBorderColor = ({
- theme,
- isOutlined,
- disabled,
- selectedColor,
-}: BaseProps & { selectedColor?: ColorValue }) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
-
- if (!isOutlined) {
- // If the Chip mode is "flat", set border color to transparent
- return 'transparent';
- }
-
- if (disabled) {
- return colors.surfaceContainer;
- }
-
- if (isSelectedColor) {
- if (typeof selectedColor === 'string') {
- return color(selectedColor).alpha(0.29).rgb().string();
- }
- // PlatformColor / OpaqueColorValue: skip the alpha pass and render opaque.
- return selectedColor;
- }
-
- return colors.outlineVariant;
-};
-
-const getTextColor = ({
- theme,
- isOutlined,
- disabled,
- selectedColor,
-}: BaseProps & {
- selectedColor?: ColorValue;
-}) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
- if (disabled) {
- return colors.onSurface;
- }
-
- if (isSelectedColor) {
- return selectedColor;
- }
-
- if (isOutlined) {
- return colors.onSurfaceVariant;
- }
-
- return colors.onSecondaryContainer;
-};
-
-const getDefaultBackgroundColor = ({
- theme,
- isOutlined,
-}: Omit) => {
- const { colors } = md3(theme);
- if (isOutlined) {
- return colors.surface;
- }
-
- return colors.secondaryContainer;
-};
-
-const getBackgroundColor = ({ theme, isOutlined, disabled }: BaseProps) => {
- const { colors } = md3(theme);
- if (disabled) {
- if (isOutlined) {
- return 'transparent';
- }
- return colors.surfaceContainerLow;
- }
-
- return getDefaultBackgroundColor({ theme, isOutlined });
-};
-
-const getIconColor = ({
- theme,
- isOutlined,
- disabled,
- selectedColor,
-}: BaseProps & {
- selectedColor?: ColorValue;
-}) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
- if (disabled) {
- return colors.onSurface;
- }
-
- if (isSelectedColor) {
- return selectedColor;
- }
-
- if (isOutlined) {
- return colors.onSurfaceVariant;
- }
-
- return colors.onSecondaryContainer;
-};
-
-export const getChipColors = ({
- isOutlined,
- theme,
- selectedColor,
- disabled,
-}: BaseProps & {
- disabled?: boolean;
- selectedColor?: ColorValue;
-}) => {
- const baseChipColorProps = { theme, isOutlined, disabled };
-
- const backgroundColor = getBackgroundColor({
- ...baseChipColorProps,
- });
-
- const contentOpacity = disabled
- ? stateOpacity.disabled
- : stateOpacity.enabled;
-
- return {
- borderColor: getBorderColor({
- ...baseChipColorProps,
- selectedColor,
- }),
- textColor: getTextColor({
- ...baseChipColorProps,
- selectedColor,
- }),
- iconColor: getIconColor({
- ...baseChipColorProps,
- selectedColor,
- }),
- contentOpacity,
- backgroundColor,
- selectedBackgroundColor: backgroundColor,
- };
-};
diff --git a/src/components/Chip/tokens.ts b/src/components/Chip/tokens.ts
new file mode 100644
index 0000000000..1c38a1a32e
--- /dev/null
+++ b/src/components/Chip/tokens.ts
@@ -0,0 +1,48 @@
+import type { ColorRole, TypescaleKey } from '../../theme/types';
+
+/**
+ * MD3 Chip component tokens.
+ * @see https://m3.material.io/components/chips/specs
+ */
+const sizes = {
+ containerHeight: 32,
+ minimumTouchTarget: 48,
+ outlineWidth: 1,
+ iconSize: 18,
+ avatarSize: 24,
+ leadingPadding: 16,
+ trailingPadding: 16,
+ iconLeadingPadding: 8,
+ avatarLeadingPadding: 4,
+ trailingIconPadding: 8,
+ leadingLabelGap: 8,
+ labelTypescale: 'labelLarge' as TypescaleKey,
+
+ disabledContentOpacity: 0.38,
+
+ flatElevation: 0,
+ elevatedElevation: 1,
+ elevatedHoverElevation: 2,
+
+ focusIndicatorThickness: 3,
+ focusIndicatorOffset: 2,
+} as const;
+
+const colors = {
+ elevatedContainerColor: 'surfaceContainerLow',
+ flatContainerColor: 'surfaceContainerLow',
+ selectedContainerColor: 'secondaryContainer',
+ outlinedContainerColor: 'surface',
+ focusIndicatorColor: 'secondary',
+ labelColor: 'onSurfaceVariant',
+ selectedLabelColor: 'onSecondaryContainer',
+ leadingIconColor: 'primary',
+ selectedIconColor: 'onSecondaryContainer',
+ trailingIconColor: 'onSurfaceVariant',
+ selectedTrailingIconColor: 'onSecondaryContainer',
+ outlineColor: 'outlineVariant',
+ focusOutlineColor: 'onSurfaceVariant',
+ disabledColor: 'onSurface',
+} as const satisfies Record;
+
+export const ChipTokens = { ...sizes, ...colors };
diff --git a/src/components/Chip/utils.ts b/src/components/Chip/utils.ts
new file mode 100644
index 0000000000..fc63fd6d76
--- /dev/null
+++ b/src/components/Chip/utils.ts
@@ -0,0 +1,207 @@
+import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
+
+import { ChipTokens } from './tokens';
+import type { InternalTheme } from '../../types';
+
+export type ChipAvatarProps = {
+ style?: StyleProp;
+};
+
+type BaseProps = {
+ theme: InternalTheme;
+ isOutlined: boolean;
+ selected?: boolean;
+ disabled?: boolean;
+ elevated?: boolean;
+ focused?: boolean;
+};
+
+const getContainerColor = ({
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ elevated,
+ customBackgroundColor,
+}: BaseProps & {
+ customBackgroundColor?: ColorValue;
+}) => {
+ if (disabled) {
+ // A disabled selected chip keeps its filled look (no outline); only a
+ // disabled unselected outlined chip stays unfilled.
+ return isOutlined && !selected
+ ? 'transparent'
+ : theme.colors.stateLayerPressed;
+ }
+
+ if (customBackgroundColor !== undefined) {
+ return customBackgroundColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedContainerColor];
+ }
+
+ if (isOutlined) {
+ return theme.colors[ChipTokens.outlinedContainerColor];
+ }
+
+ return elevated
+ ? theme.colors[ChipTokens.elevatedContainerColor]
+ : theme.colors[ChipTokens.flatContainerColor];
+};
+
+const getBorderColor = ({
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ focused,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (!isOutlined || selected) {
+ return 'transparent';
+ }
+
+ if (disabled) {
+ return theme.colors.outlineVariant;
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (focused) {
+ return theme.colors[ChipTokens.focusOutlineColor];
+ }
+
+ return theme.colors[ChipTokens.outlineColor];
+};
+
+const getLabelColor = ({
+ theme,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return theme.colors[ChipTokens.disabledColor];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedLabelColor];
+ }
+
+ return theme.colors[ChipTokens.labelColor];
+};
+
+const getLeadingIconColor = ({
+ theme,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return theme.colors[ChipTokens.disabledColor];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedIconColor];
+ }
+
+ return theme.colors[ChipTokens.leadingIconColor];
+};
+
+const getTrailingIconColor = ({
+ theme,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return theme.colors[ChipTokens.disabledColor];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedTrailingIconColor];
+ }
+
+ return theme.colors[ChipTokens.trailingIconColor];
+};
+
+export const getChipColors = ({
+ isOutlined,
+ theme,
+ selected,
+ selectedColor,
+ customBackgroundColor,
+ disabled,
+ elevated,
+ focused,
+}: BaseProps & {
+ customBackgroundColor?: ColorValue;
+ disabled?: boolean;
+ selectedColor?: ColorValue;
+}) => {
+ const baseChipColorProps = {
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ elevated,
+ focused,
+ };
+
+ const contentOpacity = disabled ? ChipTokens.disabledContentOpacity : 1;
+
+ return {
+ borderColor: getBorderColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ textColor: getLabelColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ iconColor: getLeadingIconColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ trailingIconColor: getTrailingIconColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ contentOpacity,
+ backgroundColor: getContainerColor({
+ ...baseChipColorProps,
+ customBackgroundColor,
+ }),
+ selectedBackgroundColor: getContainerColor({
+ ...baseChipColorProps,
+ selected: true,
+ customBackgroundColor,
+ }),
+ rippleColor: theme.colors.stateLayerPressed,
+ avatarOverlayColor: theme.colors.stateLayerPressed,
+ };
+};
diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx
index 87f54dba4e..bdaa51298d 100644
--- a/src/components/__tests__/Chip.test.tsx
+++ b/src/components/__tests__/Chip.test.tsx
@@ -1,13 +1,13 @@
-import { describe, expect, it } from '@jest/globals';
-import color from 'color';
+import { Text } from 'react-native';
+
+import { describe, expect, it, jest } from '@jest/globals';
+import { userEvent } from '@testing-library/react-native';
import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
-import { tokens } from '../../theme/tokens';
+import * as Avatar from '../Avatar/Avatar';
import Chip from '../Chip/Chip';
-import { getChipColors } from '../Chip/helpers';
-
-const stateOpacity = tokens.md.sys.state.opacity;
+import { getChipColors } from '../Chip/utils';
it('renders chip with onPress', async () => {
const tree = (
@@ -17,6 +17,38 @@ it('renders chip with onPress', async () => {
expect(tree).toMatchSnapshot();
});
+it('exposes the label as the accessible name of the chip button', async () => {
+ await render( {}}>Example Chip);
+
+ expect(
+ screen.getByRole('button', { name: 'Example Chip' })
+ ).toBeOnTheScreen();
+});
+
+it('exposes the accessible name for non-string children', async () => {
+ await render(
+ {}}>
+ Example Chip
+
+ );
+
+ expect(
+ screen.getByRole('button', { name: 'Example Chip' })
+ ).toBeOnTheScreen();
+});
+
+it('prefers an explicit aria-label over the derived one', async () => {
+ await render(
+ {}} aria-label="Custom label">
+ Example Chip
+
+ );
+
+ expect(
+ screen.getByRole('button', { name: 'Custom label' })
+ ).toBeOnTheScreen();
+});
+
it('renders chip with icon', async () => {
const tree = (
await render(Example Chip)
@@ -49,6 +81,51 @@ it('renders chip with custom close button', async () => {
expect(tree).toMatchSnapshot();
});
+it('renders chip with a generic trailing icon', async () => {
+ const tree = (
+ await render(
+
+ Example Chip
+
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('calls onTrailingIconPress when the trailing icon is pressed', async () => {
+ const onTrailingIconPress = jest.fn();
+ const onClose = jest.fn();
+ await render(
+
+ Example Chip
+
+ );
+
+ await userEvent.press(screen.getByTestId('chip-trailing-icon'));
+
+ expect(onTrailingIconPress).toHaveBeenCalled();
+ expect(onClose).not.toHaveBeenCalled();
+});
+
+it('prefers trailingIcon over the close icon when both trailingIcon and onClose are specified', async () => {
+ const tree = (
+ await render(
+ {}}>
+ Example Chip
+
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
it('renders outlined disabled chip', async () => {
const tree = (
await render(
@@ -67,6 +144,40 @@ it('renders selected chip', async () => {
expect(tree).toMatchSnapshot();
});
+it('renders chip with avatar', async () => {
+ const tree = (
+ await render(
+ }>Example Chip
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders selected chip with avatar and a check overlay', async () => {
+ const tree = (
+ await render(
+ }>
+ Example Chip
+
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('prefers icon over avatar when both are specified', async () => {
+ const tree = (
+ await render(
+ }>
+ Example Chip
+
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
it('renders disabled chip if there is no touch handler passed', async () => {
await render(Disabled chip);
@@ -83,6 +194,59 @@ it('renders active chip if only onLongPress handler is passed', async () => {
expect(screen.getByTestId('active-chip')).toBeEnabled();
});
+it('applies disabled opacity to the close button', async () => {
+ await render(
+ {}} testID="disabled-chip">
+ Disabled chip
+
+ );
+
+ expect(screen.getByTestId('disabled-chip-close')).toHaveStyle({
+ opacity: 0.38,
+ });
+});
+
+it('overlays the close button on top of the chip so its ripple spans behind it', async () => {
+ await render(
+ {}} onClose={() => {}} testID="chip">
+ Removable chip
+
+ );
+
+ expect(screen.getByTestId('chip-close')).toHaveStyle({
+ bottom: 0,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ });
+});
+
+it('clips the ripple to custom chip border radius', async () => {
+ await render(
+ {}} testID="rounded-chip" style={{ borderRadius: 16 }}>
+ Rounded chip
+
+ );
+
+ expect(screen.getByTestId('rounded-chip')).toHaveStyle({
+ borderRadius: 16,
+ overflow: 'hidden',
+ });
+});
+
+it('renders close button with a circular state layer', async () => {
+ await render(
+ {}} testID="chip">
+ Removable chip
+
+ );
+
+ expect(screen.getByTestId('chip-close')).toHaveStyle({
+ borderRadius: 16,
+ overflow: 'hidden',
+ });
+});
+
it('renders chip with zero border radius', async () => {
await render(
@@ -105,7 +269,7 @@ describe('getChipColors - text color', () => {
})
).toMatchObject({
textColor: getTheme().colors.onSurface,
- contentOpacity: stateOpacity.disabled,
+ contentOpacity: 0.38,
});
});
@@ -116,7 +280,7 @@ describe('getChipColors - text color', () => {
isOutlined: false,
})
).toMatchObject({
- textColor: getTheme().colors.onSecondaryContainer,
+ textColor: getTheme().colors.onSurfaceVariant,
});
});
@@ -154,7 +318,7 @@ describe('getChipColors - icon color', () => {
})
).toMatchObject({
iconColor: getTheme().colors.onSurface,
- contentOpacity: stateOpacity.disabled,
+ contentOpacity: 0.38,
});
});
@@ -165,7 +329,7 @@ describe('getChipColors - icon color', () => {
isOutlined: false,
})
).toMatchObject({
- iconColor: getTheme().colors.onSecondaryContainer,
+ iconColor: getTheme().colors.primary,
});
});
@@ -176,7 +340,7 @@ describe('getChipColors - icon color', () => {
isOutlined: true,
})
).toMatchObject({
- iconColor: getTheme().colors.onSurfaceVariant,
+ iconColor: getTheme().colors.primary,
});
});
@@ -193,6 +357,44 @@ describe('getChipColors - icon color', () => {
});
});
+describe('getChipColor - selected background color', () => {
+ it('should return custom color, outlined mode', () => {
+ expect(
+ getChipColors({
+ theme: getTheme(),
+ customBackgroundColor: 'purple',
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ selectedBackgroundColor: 'purple',
+ });
+ });
+
+ it('should return custom color, flat mode', () => {
+ expect(
+ getChipColors({
+ theme: getTheme(),
+ customBackgroundColor: 'purple',
+ isOutlined: false,
+ })
+ ).toMatchObject({
+ selectedBackgroundColor: 'purple',
+ });
+ });
+
+ it('should return theme color, for theme version 3, flat mode', () => {
+ expect(
+ getChipColors({
+ theme: getTheme(),
+ isOutlined: false,
+ selected: true,
+ })
+ ).toMatchObject({
+ selectedBackgroundColor: getTheme().colors.secondaryContainer,
+ });
+ });
+});
+
describe('getChipColor - background color', () => {
it('should return theme color, for theme version 3, outlined mode', () => {
expect(
@@ -212,7 +414,73 @@ describe('getChipColor - background color', () => {
isOutlined: false,
})
).toMatchObject({
- backgroundColor: getTheme().colors.secondaryContainer,
+ backgroundColor: getTheme().colors.surfaceContainerLow,
+ });
+ });
+
+ it('uses the precomputed state layer color for disabled filled chips', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ disabled: true,
+ isOutlined: false,
+ })
+ ).toMatchObject({
+ backgroundColor: theme.colors.stateLayerPressed,
+ });
+ });
+
+ it('keeps the filled look for a disabled selected outlined chip', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ disabled: true,
+ selected: true,
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ selectedBackgroundColor: theme.colors.stateLayerPressed,
+ });
+ });
+
+ it('stays unfilled for a disabled unselected outlined chip', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ disabled: true,
+ selected: false,
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ backgroundColor: 'transparent',
+ });
+ });
+});
+
+describe('getChipColor - ripple color', () => {
+ it('uses the precomputed state layer color', () => {
+ const theme = {
+ ...getTheme(),
+ colors: {
+ ...getTheme().colors,
+ stateLayerPressed: 'rgba(29, 27, 32, 0.1)',
+ },
+ };
+
+ expect(
+ getChipColors({
+ theme,
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ rippleColor: 'rgba(29, 27, 32, 0.1)',
+ avatarOverlayColor: 'rgba(29, 27, 32, 0.1)',
});
});
});
@@ -261,7 +529,21 @@ describe('getChipColor - border color', () => {
isOutlined: true,
})
).toMatchObject({
- borderColor: color('purple').alpha(0.29).rgb().string(),
+ borderColor: 'purple',
+ });
+ });
+
+ it('uses the tokenized outline color for disabled outlined chips', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ disabled: true,
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ borderColor: theme.colors.outlineVariant,
});
});
@@ -308,4 +590,31 @@ describe('getChipColor - border color', () => {
borderColor: 'transparent',
});
});
+
+ it('darkens the outline on focus, for theme version 3', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ isOutlined: true,
+ focused: true,
+ })
+ ).toMatchObject({
+ borderColor: theme.colors.onSurfaceVariant,
+ });
+ });
+
+ it('does not darken the outline on focus for a selected chip', () => {
+ expect(
+ getChipColors({
+ theme: getTheme(),
+ isOutlined: true,
+ selected: true,
+ focused: true,
+ })
+ ).toMatchObject({
+ borderColor: 'transparent',
+ });
+ });
});
diff --git a/src/components/__tests__/ListItem.test.tsx b/src/components/__tests__/ListItem.test.tsx
index b50f4e7d3f..b635b9f2f8 100644
--- a/src/components/__tests__/ListItem.test.tsx
+++ b/src/components/__tests__/ListItem.test.tsx
@@ -149,17 +149,18 @@ it('renders with a description with typeof number', async () => {
it('calling onPress on ListItem right component', async () => {
Platform.OS = 'web';
const onPress = jest.fn<(event: GestureResponderEvent) => void>();
+ const user = userEvent.setup();
await render(
}
/>
);
- await userEvent.press(screen.getByTestId('icon-button'));
+ await user.press(screen.getByTestId('icon-button'));
expect(onPress).toHaveBeenCalledTimes(1);
});
diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
index 04359a41f9..29c31023a4 100644
--- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
@@ -1,26 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renders chip with close button 1`] = `
+exports[`prefers icon over avatar when both are specified 1`] = `
-
-
-
- close
-
-
-
-
+ "borderWidth": 3,
+ "bottom": -2,
+ "left": -2,
+ "pointerEvents": "none",
+ "position": "absolute",
+ "right": -2,
+ "top": -2,
+ },
+ {
+ "borderColor": "rgba(98, 91, 113, 1)",
+ "borderRadius": 10,
+ },
+ {
+ "opacity": 0,
+ },
+ ]
+ }
+ />
`;
-exports[`renders chip with custom close button 1`] = `
+exports[`prefers trailingIcon over the close icon when both trailingIcon and onClose are specified 1`] = `
-
-
- information
-
-
-
-
+
-
- arrow-down
-
-
-
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ menu-down
+
-
-`;
+
+
+
+`;
+
+exports[`renders chip with a generic trailing icon 1`] = `
+
+
+
+
+
+
+ information
+
+
+
+ Example Chip
+
+
+
+
+
+ menu-down
+
+
+
+
+
+`;
+
+exports[`renders chip with avatar 1`] = `
+
+
+
+
+
+
+
+ XD
+
+
+
+
+ Example Chip
+
+
+
+
+
+`;
+
+exports[`renders chip with close button 1`] = `
+
+
+
+
+
+
+ information
+
+
+
+ Example Chip
+
+
+
+
+
+ close
+
+
+
+
+
+`;
+
+exports[`renders chip with custom close button 1`] = `
+
+
+
+
+
+
+ information
+
+
+
+ Example Chip
+
+
+
+
+
+ arrow-down
+
+
+
+
+
+`;
exports[`renders chip with icon 1`] = `
+ Example Chip
+
+
+
+
+
+`;
+
+exports[`renders chip with onPress 1`] = `
+
+
+
+
+
+
`;
-exports[`renders chip with onPress 1`] = `
+exports[`renders outlined disabled chip 1`] = `
+
`;
-exports[`renders outlined disabled chip 1`] = `
+exports[`renders selected chip 1`] = `
+
+
+ check
+
+
+
`;
-exports[`renders selected chip 1`] = `
+exports[`renders selected chip with avatar and a check overlay 1`] = `
-
+
+ XD
+
+
+
- check
-
+
+ check
+
+
+
`;
diff --git a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
index 032971c85e..ed77fc249c 100644
--- a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
@@ -123,20 +123,19 @@ exports[`renders list item with custom description 1`] = `
[
{},
{
+ "alignSelf": "flex-start",
"borderStyle": "solid",
- "borderWidth": 0.5,
- "flexDirection": "column",
- },
- {
"borderWidth": 1,
+ "justifyContent": "center",
+ "minHeight": 32,
},
{
- "borderColor": "transparent",
+ "borderColor": "rgba(202, 196, 208, 1)",
},
undefined,
{},
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "backgroundColor": "rgba(254, 247, 255, 1)",
},
{
"borderBottomEndRadius": undefined,
@@ -184,7 +183,7 @@ exports[`renders list item with custom description 1`] = `
{},
{},
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "backgroundColor": "rgba(254, 247, 255, 1)",
},
{
"borderBottomEndRadius": undefined,
@@ -235,6 +234,12 @@ exports[`renders list item with custom description 1`] = `
accessible={true}
collapsable={false}
focusable={true}
+ hitSlop={
+ {
+ "bottom": 8,
+ "top": 8,
+ }
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
@@ -252,11 +257,15 @@ exports[`renders list item with custom description 1`] = `
},
[
{
- "borderRadius": 8,
+ "alignSelf": "stretch",
+ "flexShrink": 1,
+ "justifyContent": "center",
+ "minHeight": 32,
},
{
- "width": "100%",
+ "borderRadius": 8,
},
+ undefined,
],
]
}
@@ -268,34 +277,27 @@ exports[`renders list item with custom description 1`] = `
{
"alignItems": "center",
"flexDirection": "row",
- "paddingLeft": 4,
+ "flexShrink": 1,
+ "overflow": "hidden",
"position": "relative",
},
- {
- "paddingLeft": 0,
- },
{
"opacity": 1,
- },
- {
- "paddingRight": 0,
+ "paddingLeft": 8,
+ "paddingRight": 16,
},
]
}
>
+