diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx
index ad3451e8b0..6fb043d455 100644
--- a/example/src/Examples/CardExample.tsx
+++ b/example/src/Examples/CardExample.tsx
@@ -31,13 +31,12 @@ const CardExample = () => {
{modes.map((mode) => (
setSelectedMode(mode)}
style={styles.chip}
- >
- {mode}
-
+ />
))}
{
+ const [selectedFilter, setSelectedFilter] = React.useState(filters[0]);
const [snackbarProperties, setSnackbarProperties] = React.useState({
visible: false,
text: '',
@@ -16,168 +19,65 @@ const ChipExample = () => {
return (
<>
-
+
- {}} style={styles.chip}>
- Simple
-
- (
+ setSelectedFilter(filter)}
+ style={styles.chip}
+ />
+ ))}
+ {}}
- style={styles.chip}
- >
- With selected overlay
-
- {}} style={styles.chip}>
- Elevated
-
- {}}>
- 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
-
+ />
- }
+ showSelectedCheck={false}
onPress={() => {}}
style={styles.chip}
- >
- Avatar (selected)
-
-
- setSnackbarProperties({
- visible: true,
- text: 'Disabled heart icon close button pressed',
- })
- }
- style={styles.chip}
- >
- Icon (disabled)
-
-
- }
- style={styles.chip}
- >
- Avatar (disabled)
-
+ />
+
-
+
+
- {}} style={styles.chip}>
- Simple
-
- {}}
- style={styles.chip}
- >
- With selected overlay
-
- {}}
- 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
-
+ />
+
+
+
+
+
{
}
onPress={() => {}}
style={styles.chip}
- >
- Avatar
-
+ />
{
}
onPress={() => {}}
style={styles.chip}
- >
- Avatar (selected)
-
+ />
{}}
onClose={() =>
setSnackbarProperties({
visible: true,
- text: 'Disabled close button pressed',
+ text: 'Close button pressed',
})
}
style={styles.chip}
- >
- Icon (disabled)
-
+ />
+ label="Custom close"
+ closeIcon="arrow-down"
+ onPress={() => {}}
+ onClose={() =>
+ setSnackbarProperties({
+ visible: true,
+ text: 'Custom close button pressed',
+ })
}
style={styles.chip}
- >
- Avatar (disabled)
-
+ closeIconAccessibilityLabel="Custom close icon accessibility label"
+ />
-
+
+
{}}
- compact
- avatar={
-
- }
- style={[styles.chip, styles.customBorderRadius]}
- >
- Compact with custom border radius
-
- {}}
- compact
- avatar={
-
- }
- style={[styles.chip, styles.customBorderRadius]}
- >
- Compact with custom border radius
-
- {}}
- onLongPress={() =>
- setSnackbarProperties({ visible: true, text: '' })
- }
- style={styles.chip}
- >
- With onLongPress
-
- {}}
- style={[
- styles.chip,
- {
- backgroundColor: 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,
{
backgroundColor: color(customColor).alpha(0.2).rgb().string(),
},
]}
- selectedColor={customColor}
- >
- Outlined selected chip with custom color
-
- {}}
- style={styles.chip}
- selectedColor={customColor}
- >
- 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.
-
+ />
{}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Custom icon close button pressed',
- })
- }
- closeIcon="arrow-down"
- style={styles.chip}
- closeIconAccessibilityLabel="Custom Close icon accessibility label"
- >
- With custom close icon
-
+ style={[styles.chip, styles.customBorderRadius]}
+ />
{}}
- style={styles.chip}
- textStyle={styles.tiny}
- >
- With custom text
-
+ style={styles.fullWidthChip}
+ />
- {}} style={styles.fullWidthChip}>
- Full width chip
-
({
{options.map((option) => (
onChange(option)}
- >
- {option}
-
+ />
))}
diff --git a/example/src/Examples/ListSectionExample.tsx b/example/src/Examples/ListSectionExample.tsx
index d7efbe6adc..b0d008f5f0 100644
--- a/example/src/Examples/ListSectionExample.tsx
+++ b/example/src/Examples/ListSectionExample.tsx
@@ -117,9 +117,7 @@ const ListSectionExample = () => {
- {}}>
- DOCS.pdf
-
+ {}} />
)}
diff --git a/example/src/Examples/TeamDetails.tsx b/example/src/Examples/TeamDetails.tsx
index 5970274f31..fbd7086ef3 100644
--- a/example/src/Examples/TeamDetails.tsx
+++ b/example/src/Examples/TeamDetails.tsx
@@ -58,25 +58,15 @@ const News = () => {
contentContainerStyle={styles.chipsContent}
>
{}}
style={styles.chip}
- showSelectedOverlay
- >
- Latest
-
- {}} style={styles.chip}>
- Popular
-
- {}} style={styles.chip}>
- Interviews
-
- {}} style={styles.chip}>
- Transfers
-
- {}} style={styles.chip}>
- League
-
+ />
+ {}} style={styles.chip} />
+ {}} style={styles.chip} />
+ {}} style={styles.chip} />
+ {}} style={styles.chip} />
diff --git a/example/src/Examples/TooltipExample.tsx b/example/src/Examples/TooltipExample.tsx
index 8e0802d4a4..483433331a 100644
--- a/example/src/Examples/TooltipExample.tsx
+++ b/example/src/Examples/TooltipExample.tsx
@@ -120,6 +120,7 @@ const TooltipExample = () => {
{
accessibilityIgnoresInvertColors
/>
}
- >
- John Doe
-
+ />
diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx
index 1b09c327a8..f8adfd44d6 100644
--- a/src/components/Chip/Chip.tsx
+++ b/src/components/Chip/Chip.tsx
@@ -9,39 +9,58 @@ import type {
ViewStyle,
} from 'react-native';
-import useLatestCallback from 'use-latest-callback';
-
import { getChipColors } from './helpers';
import type { ChipAvatarProps } from './helpers';
+import {
+ CHIP_AVATAR_LEADING_PADDING,
+ CHIP_AVATAR_SIZE,
+ CHIP_CLOSE_TRAILING_PADDING,
+ CHIP_CONTAINER_HEIGHT,
+ CHIP_DISABLED_CONTENT_OPACITY,
+ CHIP_ELEVATED_ELEVATION,
+ CHIP_FLAT_ELEVATION,
+ CHIP_ICON_LEADING_PADDING,
+ CHIP_LABEL_TYPESCALE,
+ CHIP_LEADING_ICON_SIZE,
+ CHIP_LEADING_LABEL_GAP,
+ CHIP_LEADING_PADDING,
+ CHIP_MINIMUM_TOUCH_TARGET,
+ CHIP_OUTLINE_WIDTH,
+ CHIP_SELECTED_ICON_SIZE,
+ CHIP_TRAILING_ICON_SIZE,
+ CHIP_TRAILING_ICON_TOUCH_TARGET,
+ CHIP_TRAILING_PADDING,
+} from './tokens';
import { useInternalTheme } from '../../core/theming';
-import { white } from '../../theme/colors';
-import type { $Omit, EllipsizeProp, Theme, ThemeProp } from '../../types';
+import type { EllipsizeProp, ThemeProp } from '../../types';
import hasTouchHandler from '../../utils/hasTouchHandler';
import type { IconSource } from '../Icon';
import Icon from '../Icon';
-import MaterialCommunityIcon from '../MaterialCommunityIcon';
import Surface from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
-export type Props = $Omit, 'mode'> & {
+export type Props = Omit<
+ React.ComponentProps,
+ 'children' | 'mode'
+> & {
/**
* 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';
/**
- * Text content of the `Chip`.
+ * Text label of the `Chip`.
*/
- children: React.ReactNode;
+ label: string;
/**
- * Icon to display for the `Chip`. Both icon and avatar cannot be specified.
+ * Leading icon to display for the `Chip`. Both icon and avatar cannot be specified.
*/
icon?: IconSource;
/**
- * Avatar to display for the `Chip`. Both icon and avatar cannot be specified.
+ * Leading avatar to display for the `Chip`. Both icon and avatar cannot be specified.
*/
avatar?: React.ReactNode;
/**
@@ -54,15 +73,9 @@ export type Props = $Omit, 'mode'> & {
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.
+ * Applies to label, leading icon, trailing icon, and custom outlined border.
*/
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`.
@@ -73,7 +86,7 @@ export type Props = $Omit, 'mode'> & {
*/
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;
@@ -110,17 +123,11 @@ export type Props = $Omit, 'mode'> & {
*/
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.
*/
elevated?: boolean;
/**
- * Style of chip's text
+ * Style of chip's text.
*/
textStyle?: StyleProp;
style?: Animated.WithAnimatedValue>;
@@ -137,7 +144,7 @@ export type Props = $Omit, 'mode'> & {
*/
testID?: string;
/**
- * Ellipsize Mode for the children text
+ * Ellipsize Mode for the label text.
*/
ellipsizeMode?: EllipsizeProp;
/**
@@ -163,7 +170,7 @@ export type Props = $Omit, 'mode'> & {
* import { Chip } from 'react-native-paper';
*
* const MyComponent = () => (
- * console.log('Pressed')}>Example Chip
+ * console.log('Pressed')} />
* );
*
* export default MyComponent;
@@ -171,7 +178,7 @@ export type Props = $Omit, 'mode'> & {
*/
const Chip = ({
mode = 'flat',
- children,
+ label,
icon,
avatar,
selected = false,
@@ -194,18 +201,13 @@ const Chip = ({
selectedColor,
showSelectedCheck = true,
ellipsizeMode,
- compact,
elevated = false,
maxFontSizeMultiplier,
hitSlop,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
- const isWeb = Platform.OS === 'web';
-
- const { current: elevation } = React.useRef(
- new Animated.Value(elevated ? 1 : 0)
- );
+ const isOutlined = mode === 'outlined';
const hasPassedTouchHandler = hasTouchHandler({
onPress,
@@ -213,35 +215,9 @@ const Chip = ({
onPressIn,
onPressOut,
});
+ const isTouchableDisabled = disabled || !hasPassedTouchHandler;
- const isOutlined = mode === 'outlined';
-
- const handlePressIn = useLatestCallback((e: GestureResponderEvent) => {
- const { scale } = theme.animation;
- onPressIn?.(e);
- Animated.timing(elevation, {
- toValue: elevated ? 2 : 0,
- duration: 200 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
- });
-
- const handlePressOut = useLatestCallback((e: GestureResponderEvent) => {
- const { scale } = theme.animation;
- onPressOut?.(e);
- Animated.timing(elevation, {
- toValue: elevated ? 1 : 0,
- duration: 150 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
- });
-
- const opacity = 0.38;
const defaultBorderRadius = theme.shapes.corner.small;
- const iconSize = 18;
-
const {
backgroundColor: customBackgroundColor,
borderRadius = defaultBorderRadius,
@@ -251,38 +227,51 @@ const Chip = ({
borderColor,
textColor,
iconColor,
+ closeIconColor,
contentOpacity,
selectedBackgroundColor,
backgroundColor,
+ rippleColor,
+ avatarOverlayColor,
} = getChipColors({
isOutlined,
+ selected,
+ elevated,
theme,
selectedColor,
customBackgroundColor,
disabled,
});
- const elevationStyle = elevation;
- const multiplier = compact ? 1.5 : 2;
- const labelSpacings = {
- marginRight: onClose ? 0 : 8 * multiplier,
- marginLeft:
- avatar || icon || (selected && showSelectedCheck)
- ? 4 * multiplier
- : 8 * multiplier,
- };
- const contentSpacings = {
- paddingRight: onClose ? 34 : 0,
- };
- const labelTextStyle = {
- color: textColor,
- ...(theme as Theme).fonts.labelLarge,
+ const hasAvatar = !!avatar && !icon;
+ const showSelectedIcon = selected && showSelectedCheck && !icon;
+ const showLeadingIcon = !!icon || showSelectedIcon;
+ const hasLeading = hasAvatar || showLeadingIcon;
+ const hasClose = !!onClose;
+
+ const leftPadding = hasAvatar
+ ? CHIP_AVATAR_LEADING_PADDING
+ : hasLeading
+ ? CHIP_ICON_LEADING_PADDING
+ : CHIP_LEADING_PADDING;
+ const rightPadding = hasClose
+ ? CHIP_CLOSE_TRAILING_PADDING
+ : CHIP_TRAILING_PADDING;
+ const touchTargetInset =
+ (CHIP_MINIMUM_TOUCH_TARGET - CHIP_CONTAINER_HEIGHT) / 2;
+ const touchTargetHitSlop = {
+ top: touchTargetInset,
+ bottom: touchTargetInset,
};
+ const closeAndroidRipple =
+ Platform.OS === 'android'
+ ? { color: rippleColor, borderless: false }
+ : undefined;
+
return (
- {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}
- {children}
+ {label}
- {onClose ? (
-
-
-
- {closeIcon ? (
-
- ) : (
-
- )}
-
-
-
+ {hasClose ? (
+ [
+ styles.closeButton,
+ disabled ? { opacity: contentOpacity } : null,
+ Platform.OS === 'web' && pressed
+ ? { backgroundColor: rippleColor }
+ : null,
+ ]}
+ >
+
+
) : null}
);
@@ -420,72 +394,61 @@ const Chip = ({
const styles = StyleSheet.create({
container: {
- borderWidth: StyleSheet.hairlineWidth,
+ height: CHIP_CONTAINER_HEIGHT,
+ borderWidth: CHIP_OUTLINE_WIDTH,
borderStyle: 'solid',
- flexDirection: Platform.select({ default: 'column', web: 'row' }),
+ flexDirection: 'row',
+ alignItems: 'center',
+ alignSelf: 'flex-start',
},
- md3Container: {
- borderWidth: 1,
+ touchable: {
+ height: '100%',
+ flexGrow: 1,
+ flexShrink: 1,
},
content: {
+ height: '100%',
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: CHIP_AVATAR_SIZE,
+ height: CHIP_AVATAR_SIZE,
+ borderRadius: CHIP_AVATAR_SIZE / 2,
+ marginRight: CHIP_LEADING_LABEL_GAP,
+ overflow: 'hidden',
},
avatar: {
- width: 24,
- height: 24,
- borderRadius: 12,
- },
- avatarWrapper: {
- marginRight: 4,
+ width: CHIP_AVATAR_SIZE,
+ height: CHIP_AVATAR_SIZE,
+ borderRadius: CHIP_AVATAR_SIZE / 2,
},
- md3AvatarWrapper: {
- marginLeft: 4,
- marginRight: 0,
+ avatarSelectedOverlay: {
+ ...StyleSheet.absoluteFill,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- md3SelectedIcon: {
- paddingLeft: 4,
+ leadingIcon: {
+ width: CHIP_LEADING_ICON_SIZE,
+ height: CHIP_LEADING_ICON_SIZE,
+ marginRight: CHIP_LEADING_LABEL_GAP,
+ 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: {
- position: 'absolute',
- right: 0,
+ closeButton: {
+ width: CHIP_TRAILING_ICON_TOUCH_TARGET,
height: '100%',
- justifyContent: 'center',
alignItems: 'center',
+ justifyContent: 'center',
},
- touchable: {
- width: '100%',
+ disabled: {
+ opacity: CHIP_DISABLED_CONTENT_OPACITY,
},
});
diff --git a/src/components/Chip/helpers.tsx b/src/components/Chip/helpers.tsx
index 4b0fdf9e06..d700d2de22 100644
--- a/src/components/Chip/helpers.tsx
+++ b/src/components/Chip/helpers.tsx
@@ -1,13 +1,21 @@
import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
-import color from 'color';
-
-import { tokens } from '../../theme/tokens';
-import type { InternalTheme, Theme } from '../../types';
-
-const md3 = (theme: InternalTheme) => theme as Theme;
-
-const stateOpacity = tokens.md.sys.state.opacity;
+import {
+ CHIP_DISABLED_COLOR,
+ CHIP_DISABLED_CONTENT_OPACITY,
+ CHIP_ELEVATED_CONTAINER_COLOR,
+ CHIP_FLAT_CONTAINER_COLOR,
+ CHIP_LABEL_COLOR,
+ CHIP_LEADING_ICON_COLOR,
+ CHIP_OUTLINE_COLOR,
+ CHIP_OUTLINED_CONTAINER_COLOR,
+ CHIP_SELECTED_CONTAINER_COLOR,
+ CHIP_SELECTED_ICON_COLOR,
+ CHIP_SELECTED_LABEL_COLOR,
+ CHIP_SELECTED_TRAILING_ICON_COLOR,
+ CHIP_TRAILING_ICON_COLOR,
+} from './tokens';
+import type { InternalTheme } from '../../types';
export type ChipAvatarProps = {
style?: StyleProp;
@@ -16,182 +24,186 @@ export type ChipAvatarProps = {
type BaseProps = {
theme: InternalTheme;
isOutlined: boolean;
+ selected?: boolean;
disabled?: boolean;
+ elevated?: boolean;
};
-const getBorderColor = ({
+const getContainerColor = ({
theme,
isOutlined,
+ selected,
disabled,
- selectedColor,
-}: BaseProps & { backgroundColor: ColorValue; selectedColor?: ColorValue }) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
+ elevated,
+ customBackgroundColor,
+}: BaseProps & {
+ customBackgroundColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return isOutlined ? 'transparent' : theme.colors.stateLayerPressed;
+ }
- if (!isOutlined) {
- // If the Chip mode is "flat", set border color to transparent
- return 'transparent';
+ if (customBackgroundColor !== undefined) {
+ return customBackgroundColor;
}
- if (disabled) {
- return colors.surfaceContainer;
+ if (selected) {
+ return theme.colors[CHIP_SELECTED_CONTAINER_COLOR];
}
- 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;
+ if (isOutlined) {
+ return theme.colors[CHIP_OUTLINED_CONTAINER_COLOR];
}
- return colors.outlineVariant;
+ return elevated
+ ? theme.colors[CHIP_ELEVATED_CONTAINER_COLOR]
+ : theme.colors[CHIP_FLAT_CONTAINER_COLOR];
};
-const getTextColor = ({
+const getBorderColor = ({
theme,
isOutlined,
+ selected,
disabled,
selectedColor,
}: BaseProps & {
selectedColor?: ColorValue;
}) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
- if (disabled) {
- return colors.onSurface;
- }
-
- if (isSelectedColor) {
- return selectedColor;
+ if (!isOutlined || selected) {
+ return 'transparent';
}
- if (isOutlined) {
- return colors.onSurfaceVariant;
+ if (disabled) {
+ return theme.colors.outlineVariant;
}
- return colors.onSecondaryContainer;
-};
-
-const getDefaultBackgroundColor = ({
- theme,
- isOutlined,
-}: Omit) => {
- const { colors } = md3(theme);
- if (isOutlined) {
- return colors.surface;
+ if (selectedColor !== undefined) {
+ return selectedColor;
}
- return colors.secondaryContainer;
+ return theme.colors[CHIP_OUTLINE_COLOR];
};
-const getBackgroundColor = ({
+const getLabelColor = ({
theme,
- isOutlined,
+ selected,
disabled,
- customBackgroundColor,
+ selectedColor,
}: BaseProps & {
- customBackgroundColor?: ColorValue;
+ selectedColor?: ColorValue;
}) => {
- const { colors } = md3(theme);
- if (typeof customBackgroundColor === 'string') {
- return customBackgroundColor;
+ if (disabled) {
+ return theme.colors[CHIP_DISABLED_COLOR];
}
- if (disabled) {
- if (isOutlined) {
- return 'transparent';
- }
- return colors.surfaceContainerLow;
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[CHIP_SELECTED_LABEL_COLOR];
}
- return getDefaultBackgroundColor({ theme, isOutlined });
+ return theme.colors[CHIP_LABEL_COLOR];
};
-const getSelectedBackgroundColor = ({
+const getLeadingIconColor = ({
theme,
- isOutlined,
+ selected,
disabled,
- customBackgroundColor,
+ selectedColor,
}: BaseProps & {
- customBackgroundColor?: ColorValue;
+ selectedColor?: ColorValue;
}) => {
- return getBackgroundColor({
- theme,
- disabled,
- isOutlined,
- customBackgroundColor,
- });
+ if (disabled) {
+ return theme.colors[CHIP_DISABLED_COLOR];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[CHIP_SELECTED_ICON_COLOR];
+ }
+
+ return theme.colors[CHIP_LEADING_ICON_COLOR];
};
-const getIconColor = ({
+const getTrailingIconColor = ({
theme,
- isOutlined,
+ selected,
disabled,
selectedColor,
}: BaseProps & {
selectedColor?: ColorValue;
}) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
if (disabled) {
- return colors.onSurface;
+ return theme.colors[CHIP_DISABLED_COLOR];
}
- if (isSelectedColor) {
+ if (selectedColor !== undefined) {
return selectedColor;
}
- if (isOutlined) {
- return colors.onSurfaceVariant;
+ if (selected) {
+ return theme.colors[CHIP_SELECTED_TRAILING_ICON_COLOR];
}
- return colors.onSecondaryContainer;
+ return theme.colors[CHIP_TRAILING_ICON_COLOR];
};
export const getChipColors = ({
isOutlined,
theme,
+ selected,
selectedColor,
customBackgroundColor,
disabled,
+ elevated,
}: BaseProps & {
customBackgroundColor?: ColorValue;
disabled?: boolean;
selectedColor?: ColorValue;
}) => {
- const baseChipColorProps = { theme, isOutlined, disabled };
-
- const backgroundColor = getBackgroundColor({
- ...baseChipColorProps,
- customBackgroundColor,
- });
-
- const selectedBackgroundColor = getSelectedBackgroundColor({
- ...baseChipColorProps,
- customBackgroundColor,
- });
+ const baseChipColorProps = {
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ elevated,
+ };
- const contentOpacity = disabled
- ? stateOpacity.disabled
- : stateOpacity.enabled;
+ const contentOpacity = disabled ? CHIP_DISABLED_CONTENT_OPACITY : 1;
return {
borderColor: getBorderColor({
...baseChipColorProps,
selectedColor,
- backgroundColor,
}),
- textColor: getTextColor({
+ textColor: getLabelColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ iconColor: getLeadingIconColor({
...baseChipColorProps,
selectedColor,
}),
- iconColor: getIconColor({
+ closeIconColor: getTrailingIconColor({
...baseChipColorProps,
selectedColor,
}),
contentOpacity,
- backgroundColor,
- selectedBackgroundColor,
+ backgroundColor: getContainerColor({
+ ...baseChipColorProps,
+ customBackgroundColor,
+ }),
+ selectedBackgroundColor: getContainerColor({
+ ...baseChipColorProps,
+ selected: true,
+ customBackgroundColor,
+ }),
+ rippleColor: theme.colors.stateLayerPressed,
+ avatarOverlayColor: theme.colors.stateLayerPressed,
};
};
diff --git a/src/components/Chip/tokens.ts b/src/components/Chip/tokens.ts
new file mode 100644
index 0000000000..d69d3f2802
--- /dev/null
+++ b/src/components/Chip/tokens.ts
@@ -0,0 +1,40 @@
+import type { ColorRole, Elevation, TypescaleKey } from '../../theme/types';
+
+/**
+ * MD3 Chip component tokens.
+ * @see https://m3.material.io/components/chips/specs
+ */
+export const CHIP_CONTAINER_HEIGHT = 32;
+export const CHIP_MINIMUM_TOUCH_TARGET = 48;
+export const CHIP_OUTLINE_WIDTH = 1;
+export const CHIP_LEADING_ICON_SIZE = 18;
+export const CHIP_TRAILING_ICON_SIZE = 18;
+export const CHIP_AVATAR_SIZE = 24;
+export const CHIP_SELECTED_ICON_SIZE = 18;
+export const CHIP_LEADING_PADDING = 16;
+export const CHIP_TRAILING_PADDING = 16;
+export const CHIP_ICON_LEADING_PADDING = 8;
+export const CHIP_AVATAR_LEADING_PADDING = 4;
+export const CHIP_CLOSE_TRAILING_PADDING = 8;
+export const CHIP_LEADING_LABEL_GAP = 8;
+export const CHIP_TRAILING_ICON_TOUCH_TARGET = 32;
+export const CHIP_LABEL_TYPESCALE: TypescaleKey = 'labelLarge';
+
+export const CHIP_ELEVATED_CONTAINER_COLOR: ColorRole = 'surfaceContainerLow';
+export const CHIP_FLAT_CONTAINER_COLOR: ColorRole = 'surfaceContainerLow';
+export const CHIP_SELECTED_CONTAINER_COLOR: ColorRole = 'secondaryContainer';
+export const CHIP_OUTLINED_CONTAINER_COLOR: ColorRole = 'surface';
+export const CHIP_LABEL_COLOR: ColorRole = 'onSurfaceVariant';
+export const CHIP_SELECTED_LABEL_COLOR: ColorRole = 'onSecondaryContainer';
+export const CHIP_LEADING_ICON_COLOR: ColorRole = 'primary';
+export const CHIP_SELECTED_ICON_COLOR: ColorRole = 'onSecondaryContainer';
+export const CHIP_TRAILING_ICON_COLOR: ColorRole = 'onSurfaceVariant';
+export const CHIP_SELECTED_TRAILING_ICON_COLOR: ColorRole =
+ 'onSecondaryContainer';
+export const CHIP_OUTLINE_COLOR: ColorRole = 'outlineVariant';
+export const CHIP_DISABLED_COLOR: ColorRole = 'onSurface';
+
+export const CHIP_DISABLED_CONTENT_OPACITY = 0.38;
+
+export const CHIP_FLAT_ELEVATION: Elevation = 0;
+export const CHIP_ELEVATED_ELEVATION: Elevation = 1;
diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx
index 644906ae33..6a9b71d109 100644
--- a/src/components/__tests__/Chip.test.tsx
+++ b/src/components/__tests__/Chip.test.tsx
@@ -2,19 +2,15 @@ import { Animated } from 'react-native';
import { describe, expect, it, jest } from '@jest/globals';
import { act } from '@testing-library/react-native';
-import color from 'color';
import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
-import { tokens } from '../../theme/tokens';
import Chip from '../Chip/Chip';
import { getChipColors } from '../Chip/helpers';
-const stateOpacity = tokens.md.sys.state.opacity;
-
it('renders chip with onPress', async () => {
const tree = (
- await render( {}}>Example Chip)
+ await render( {}} />)
).toJSON();
expect(tree).toMatchSnapshot();
@@ -22,7 +18,7 @@ it('renders chip with onPress', async () => {
it('renders chip with icon', async () => {
const tree = (
- await render(Example Chip)
+ await render()
).toJSON();
expect(tree).toMatchSnapshot();
@@ -31,9 +27,7 @@ it('renders chip with icon', async () => {
it('renders chip with close button', async () => {
const tree = (
await render(
- {}}>
- Example Chip
-
+ {}} />
)
).toJSON();
@@ -43,9 +37,12 @@ it('renders chip with close button', async () => {
it('renders chip with custom close button', async () => {
const tree = (
await render(
- {}} closeIcon="arrow-down">
- Example Chip
-
+ {}}
+ closeIcon="arrow-down"
+ />
)
).toJSON();
@@ -54,43 +51,54 @@ it('renders chip with custom close button', async () => {
it('renders outlined disabled chip', async () => {
const tree = (
- await render(
-
- Example Chip
-
- )
+ await render()
).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders selected chip', async () => {
- const tree = (await render(Example Chip)).toJSON();
+ const tree = (await render()).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders disabled chip if there is no touch handler passed', async () => {
- await render(Disabled chip);
+ await render();
expect(screen.getByTestId('disabled-chip')).toBeDisabled();
});
it('renders active chip if only onLongPress handler is passed', async () => {
await render(
- {}} testID="active-chip">
- Active chip
-
+ {}} testID="active-chip" />
);
expect(screen.getByTestId('active-chip')).toBeEnabled();
});
+it('applies disabled opacity to the close button', async () => {
+ await render(
+ {}}
+ testID="disabled-chip"
+ />
+ );
+
+ expect(screen.getByTestId('disabled-chip-close')).toHaveStyle({
+ opacity: 0.38,
+ });
+});
+
it('renders chip with zero border radius', async () => {
await render(
-
- Active chip
-
+
);
expect(screen.getByTestId('active-chip')).toHaveStyle({
@@ -108,7 +116,7 @@ describe('getChipColors - text color', () => {
})
).toMatchObject({
textColor: getTheme().colors.onSurface,
- contentOpacity: stateOpacity.disabled,
+ contentOpacity: 0.38,
});
});
@@ -119,7 +127,7 @@ describe('getChipColors - text color', () => {
isOutlined: false,
})
).toMatchObject({
- textColor: getTheme().colors.onSecondaryContainer,
+ textColor: getTheme().colors.onSurfaceVariant,
});
});
@@ -157,7 +165,7 @@ describe('getChipColors - icon color', () => {
})
).toMatchObject({
iconColor: getTheme().colors.onSurface,
- contentOpacity: stateOpacity.disabled,
+ contentOpacity: 0.38,
});
});
@@ -168,7 +176,7 @@ describe('getChipColors - icon color', () => {
isOutlined: false,
})
).toMatchObject({
- iconColor: getTheme().colors.onSecondaryContainer,
+ iconColor: getTheme().colors.primary,
});
});
@@ -179,7 +187,7 @@ describe('getChipColors - icon color', () => {
isOutlined: true,
})
).toMatchObject({
- iconColor: getTheme().colors.onSurfaceVariant,
+ iconColor: getTheme().colors.primary,
});
});
@@ -226,6 +234,7 @@ describe('getChipColor - selected background color', () => {
getChipColors({
theme: getTheme(),
isOutlined: false,
+ selected: true,
})
).toMatchObject({
selectedBackgroundColor: getTheme().colors.secondaryContainer,
@@ -264,7 +273,43 @@ 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,
+ });
+ });
+});
+
+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)',
});
});
});
@@ -313,7 +358,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,
});
});
@@ -378,12 +437,11 @@ it('animated value changes correctly', async () => {
const value = new Animated.Value(1);
await render(
{}}
testID="chip"
style={[{ transform: [{ scale: value }] }]}
- >
- Example Chip
-
+ />
);
expect(screen.getByTestId('chip-container-outer-layer')).toHaveStyle({
transform: [{ scale: 1 }],
diff --git a/src/components/__tests__/ListItem.test.tsx b/src/components/__tests__/ListItem.test.tsx
index b50f4e7d3f..faff763af8 100644
--- a/src/components/__tests__/ListItem.test.tsx
+++ b/src/components/__tests__/ListItem.test.tsx
@@ -116,9 +116,7 @@ it('renders list item with custom description', async () => {
Design library that has you covered in all major use-cases.
- {}}>
- DOCS.pdf
-
+ {}} />
)}
@@ -149,17 +147,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 7bf18dde0e..3fb7415bc9 100644
--- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
@@ -5,8 +5,10 @@ exports[`renders chip with close button 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignSelf": "flex-start",
+ "backgroundColor": "rgba(247, 242, 250, 1)",
"borderRadius": 8,
+ "height": 32,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -22,13 +24,14 @@ exports[`renders chip with close button 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignItems": "center",
+ "backgroundColor": "rgba(247, 242, 250, 1)",
"borderColor": "transparent",
"borderRadius": 8,
"borderStyle": "solid",
"borderWidth": 1,
- "flex": undefined,
- "flexDirection": "column",
+ "flex": 1,
+ "flexDirection": "row",
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -41,6 +44,7 @@ exports[`renders chip with close button 1`] = `
testID="chip-container"
>
-
-
+
-
- close
-
-
-
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ close
+
@@ -303,8 +288,10 @@ exports[`renders chip with custom close button 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignSelf": "flex-start",
+ "backgroundColor": "rgba(247, 242, 250, 1)",
"borderRadius": 8,
+ "height": 32,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -320,13 +307,14 @@ exports[`renders chip with custom close button 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignItems": "center",
+ "backgroundColor": "rgba(247, 242, 250, 1)",
"borderColor": "transparent",
"borderRadius": 8,
"borderStyle": "solid",
"borderWidth": 1,
- "flex": undefined,
- "flexDirection": "column",
+ "flex": 1,
+ "flexDirection": "row",
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -339,6 +327,7 @@ exports[`renders chip with custom close button 1`] = `
testID="chip-container"
>
-
-
+
-
- arrow-down
-
-
-
+ },
+ {
+ "backgroundColor": "transparent",
+ },
+ ],
+ ]
+ }
+ >
+ arrow-down
+
@@ -601,8 +571,10 @@ exports[`renders chip with icon 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignSelf": "flex-start",
+ "backgroundColor": "rgba(247, 242, 250, 1)",
"borderRadius": 8,
+ "height": 32,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -618,13 +590,14 @@ exports[`renders chip with icon 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignItems": "center",
+ "backgroundColor": "rgba(247, 242, 250, 1)",
"borderColor": "transparent",
"borderRadius": 8,
"borderStyle": "solid",
"borderWidth": 1,
- "flex": undefined,
- "flexDirection": "column",
+ "flex": 1,
+ "flexDirection": "row",
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -637,6 +610,7 @@ exports[`renders chip with icon 1`] = `
testID="chip-container"
>