diff --git a/example/src/Examples/ChipExample.tsx b/example/src/Examples/ChipExample.tsx index 8ee757fa0e..5483f35258 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,51 +19,62 @@ 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 + No check - {}} style={styles.chip}> - Elevated + + Disabled - {}}> - Compact chip + + + + + + {}} style={styles.chip}> + Outlined {}} - onClose={() => - setSnackbarProperties({ - visible: true, - text: 'Close button pressed', - }) - } style={styles.chip} - closeIconAccessibilityLabel="Close icon accessibility label" > - Close button + Flat {}} - onClose={() => - setSnackbarProperties({ - visible: true, - text: 'Heart icon close button pressed', - }) - } style={styles.chip} > - Icon + Elevated + + + + + { /> } onPress={() => {}} - onClose={() => - setSnackbarProperties({ - visible: true, - text: 'Avatar close button pressed', - }) - } style={styles.chip} > Avatar @@ -90,67 +98,10 @@ const ChipExample = () => { onPress={() => {}} style={styles.chip} > - Avatar (selected) - - - setSnackbarProperties({ - visible: true, - text: 'Disabled heart icon close button pressed', - }) - } - style={styles.chip} - > - Icon (disabled) + Selected avatar - } - 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({ @@ -160,208 +111,52 @@ const ChipExample = () => { } style={styles.chip} > - Close button + Removable {}} onClose={() => setSnackbarProperties({ visible: true, - text: 'Heart icon close button pressed', + text: 'Custom close button pressed', }) } style={styles.chip} + closeIconAccessibilityLabel="Custom close icon accessibility label" > - Icon - - - } - onPress={() => {}} - style={styles.chip} - > - Avatar - - - } - onPress={() => {}} - style={styles.chip} - > - Avatar (selected) - - - setSnackbarProperties({ - visible: true, - text: 'Disabled close button pressed', - }) - } - style={styles.chip} - > - Icon (disabled) - - - } - style={styles.chip} - > - Avatar (disabled) + Custom close - + + - {}} - 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. + 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 1b09c327a8..bfd142ba12 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native'; +import { Animated, Platform, StyleSheet, View } from 'react-native'; import type { ColorValue, GestureResponderEvent, @@ -9,27 +9,43 @@ 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, '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'; /** @@ -37,11 +53,11 @@ export type Props = $Omit, 'mode'> & { */ children: React.ReactNode; /** - * 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 +70,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 +83,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 +120,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 +141,7 @@ export type Props = $Omit, 'mode'> & { */ testID?: string; /** - * Ellipsize Mode for the children text + * Ellipsize Mode for the label text. */ ellipsizeMode?: EllipsizeProp; /** @@ -170,7 +174,7 @@ export type Props = $Omit, 'mode'> & { * ``` */ const Chip = ({ - mode = 'flat', + mode = 'outlined', children, icon, avatar, @@ -194,18 +198,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 +212,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 +224,55 @@ 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: true, + radius: CHIP_TRAILING_ICON_TOUCH_TARGET / 2, + } + : undefined; + return ( - - {avatar && !icon ? ( - - {React.isValidElement(avatar) - ? React.cloneElement(avatar, { - style: [styles.avatar, avatar.props.style], - }) - : avatar} - - ) : null} - {icon || (selected && showSelectedCheck) ? ( - - {icon ? ( + + + + {hasAvatar ? ( + + {React.isValidElement(avatar) + ? React.cloneElement(avatar, { + style: [styles.avatar, avatar.props.style], + }) + : avatar} + {showSelectedIcon ? ( + - ) : ( - - )} - - ) : null} - - {children} - - - - {onClose ? ( - - - - {closeIcon ? ( - - ) : ( - - )} - - - + + ) : null} + + ) : null} + {showLeadingIcon && !hasAvatar ? ( + + + + ) : null} + + {children} + + + {hasClose ? ( + + + ) : null} ); @@ -420,72 +398,71 @@ 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' }), - }, - md3Container: { - borderWidth: 1, - }, - content: { flexDirection: 'row', alignItems: 'center', - paddingLeft: 4, - position: 'relative', + alignSelf: 'flex-start', }, - md3Content: { - paddingLeft: 0, - }, - icon: { - padding: 4, - alignSelf: 'center', + touchable: { + height: '100%', + flexGrow: 1, + flexShrink: 1, }, - md3Icon: { - paddingLeft: 8, - paddingRight: 0, + rippleLayer: { + ...StyleSheet.absoluteFill, }, - closeIcon: { - marginRight: 4, + rippleContent: { + flex: 1, }, - md3CloseIcon: { - marginRight: 8, - padding: 0, + content: { + height: '100%', + flexGrow: 1, + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + position: 'relative', + overflow: 'hidden', }, - 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, + width: CHIP_AVATAR_SIZE, + height: CHIP_AVATAR_SIZE, + borderRadius: CHIP_AVATAR_SIZE / 2, }, - avatarWrapper: { - marginRight: 4, - }, - 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', + borderRadius: CHIP_TRAILING_ICON_TOUCH_TARGET / 2, + overflow: 'hidden', 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..d462f130b0 --- /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 = 'outline'; +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..8988b9047b 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -2,16 +2,12 @@ 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) @@ -86,6 +82,60 @@ 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('spans the chip ripple behind the close button', async () => { + await render( + {}} onClose={() => {}} testID="chip"> + Removable chip + + ); + + expect(screen.getByTestId('chip')).toHaveStyle({ + bottom: 0, + left: 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( @@ -108,7 +158,7 @@ describe('getChipColors - text color', () => { }) ).toMatchObject({ textColor: getTheme().colors.onSurface, - contentOpacity: stateOpacity.disabled, + contentOpacity: 0.38, }); }); @@ -119,7 +169,7 @@ describe('getChipColors - text color', () => { isOutlined: false, }) ).toMatchObject({ - textColor: getTheme().colors.onSecondaryContainer, + textColor: getTheme().colors.onSurfaceVariant, }); }); @@ -157,7 +207,7 @@ describe('getChipColors - icon color', () => { }) ).toMatchObject({ iconColor: getTheme().colors.onSurface, - contentOpacity: stateOpacity.disabled, + contentOpacity: 0.38, }); }); @@ -168,7 +218,7 @@ describe('getChipColors - icon color', () => { isOutlined: false, }) ).toMatchObject({ - iconColor: getTheme().colors.onSecondaryContainer, + iconColor: getTheme().colors.primary, }); }); @@ -179,7 +229,7 @@ describe('getChipColors - icon color', () => { isOutlined: true, }) ).toMatchObject({ - iconColor: getTheme().colors.onSurfaceVariant, + iconColor: getTheme().colors.primary, }); }); @@ -226,6 +276,7 @@ describe('getChipColor - selected background color', () => { getChipColors({ theme: getTheme(), isOutlined: false, + selected: true, }) ).toMatchObject({ selectedBackgroundColor: getTheme().colors.secondaryContainer, @@ -264,7 +315,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 +400,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, }); }); @@ -336,7 +437,7 @@ describe('getChipColor - border color', () => { isOutlined: true, }) ).toMatchObject({ - borderColor: getTheme(false).colors.outlineVariant, + borderColor: getTheme(false).colors.outline, }); }); @@ -347,7 +448,7 @@ describe('getChipColor - border color', () => { isOutlined: true, }) ).toMatchObject({ - borderColor: getTheme(true).colors.outlineVariant, + borderColor: getTheme(true).colors.outline, }); }); 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 7bf18dde0e..f61fe159ed 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(254, 247, 255, 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)", - "borderColor": "transparent", + "alignItems": "center", + "backgroundColor": "rgba(254, 247, 255, 1)", + "borderColor": "rgba(121, 116, 126, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -61,6 +64,12 @@ exports[`renders chip with close button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -78,10 +87,19 @@ exports[`renders chip with close button 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", }, { - "width": "100%", + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "borderRadius": 8, }, ], ] @@ -90,209 +108,199 @@ exports[`renders chip with close button 1`] = ` > - + + + - - information - - + } + > - Example Chip + information + + Example Chip + - - + - - close - - - + }, + { + "backgroundColor": "transparent", + }, + ], + ] + } + > + close + @@ -303,8 +311,10 @@ exports[`renders chip with custom close button 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", + "alignSelf": "flex-start", + "backgroundColor": "rgba(254, 247, 255, 1)", "borderRadius": 8, + "height": 32, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -320,13 +330,14 @@ exports[`renders chip with custom close button 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", - "borderColor": "transparent", + "alignItems": "center", + "backgroundColor": "rgba(254, 247, 255, 1)", + "borderColor": "rgba(121, 116, 126, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -359,6 +370,12 @@ exports[`renders chip with custom close button 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -376,10 +393,19 @@ exports[`renders chip with custom close button 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", + }, + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, }, { - "width": "100%", + "borderRadius": 8, }, ], ] @@ -388,209 +414,199 @@ exports[`renders chip with custom close button 1`] = ` > - + + + - - information - - + } + > - Example Chip + information + + Example Chip + - - + - - arrow-down - - - + }, + { + "backgroundColor": "transparent", + }, + ], + ] + } + > + arrow-down + @@ -601,8 +617,10 @@ exports[`renders chip with icon 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", + "alignSelf": "flex-start", + "backgroundColor": "rgba(254, 247, 255, 1)", "borderRadius": 8, + "height": 32, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -618,13 +636,14 @@ exports[`renders chip with icon 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", - "borderColor": "transparent", + "alignItems": "center", + "backgroundColor": "rgba(254, 247, 255, 1)", + "borderColor": "rgba(121, 116, 126, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -657,6 +676,12 @@ exports[`renders chip with icon 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -674,128 +699,131 @@ exports[`renders chip with icon 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", + }, + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, }, { - "width": "100%", + "borderRadius": 8, }, ], ] } - testID="chip" + testID="chip" + > + + + - - - information - - - Example Chip + information + + Example Chip + @@ -806,8 +834,10 @@ exports[`renders chip with onPress 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", + "alignSelf": "flex-start", + "backgroundColor": "rgba(254, 247, 255, 1)", "borderRadius": 8, + "height": 32, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -823,13 +853,14 @@ exports[`renders chip with onPress 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", - "borderColor": "transparent", + "alignItems": "center", + "backgroundColor": "rgba(254, 247, 255, 1)", + "borderColor": "rgba(121, 116, 126, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -862,6 +893,12 @@ exports[`renders chip with onPress 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -879,10 +916,19 @@ exports[`renders chip with onPress 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", + }, + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, }, { - "width": "100%", + "borderRadius": 8, }, ], ] @@ -890,72 +936,70 @@ exports[`renders chip with onPress 1`] = ` testID="chip" > + + + - - Example Chip - - + ], + ] + } + > + Example Chip + @@ -966,8 +1010,10 @@ exports[`renders outlined disabled chip 1`] = ` collapsable={false} style={ { + "alignSelf": "flex-start", "backgroundColor": "transparent", "borderRadius": 8, + "height": 32, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -983,13 +1029,14 @@ exports[`renders outlined disabled chip 1`] = ` collapsable={false} style={ { + "alignItems": "center", "backgroundColor": "transparent", - "borderColor": "rgba(243, 237, 247, 1)", + "borderColor": "rgba(202, 196, 208, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -1022,6 +1069,12 @@ exports[`renders outlined disabled chip 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1039,10 +1092,19 @@ exports[`renders outlined disabled chip 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", + }, + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, }, { - "width": "100%", + "borderRadius": 8, }, ], ] @@ -1050,72 +1112,70 @@ exports[`renders outlined disabled chip 1`] = ` testID="chip" > + + + - - Example Chip - - + ], + ] + } + > + Example Chip + @@ -1126,8 +1186,10 @@ exports[`renders selected chip 1`] = ` collapsable={false} style={ { + "alignSelf": "flex-start", "backgroundColor": "rgba(232, 222, 248, 1)", "borderRadius": 8, + "height": 32, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -1143,13 +1205,14 @@ exports[`renders selected chip 1`] = ` collapsable={false} style={ { + "alignItems": "center", "backgroundColor": "rgba(232, 222, 248, 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, @@ -1182,6 +1245,12 @@ exports[`renders selected chip 1`] = ` accessible={true} collapsable={false} focusable={true} + hitSlop={ + { + "bottom": 8, + "top": 8, + } + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -1199,10 +1268,19 @@ exports[`renders selected chip 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", + }, + { + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, }, { - "width": "100%", + "borderRadius": 8, }, ], ] @@ -1211,116 +1289,110 @@ exports[`renders selected chip 1`] = ` > - + + + - - check - - + } + > - Example Chip + check + + Example Chip + diff --git a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap index f06b87045f..c83f9df476 100644 --- a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap @@ -121,8 +121,10 @@ exports[`renders list item with custom description 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", + "alignSelf": "flex-start", + "backgroundColor": "rgba(254, 247, 255, 1)", "borderRadius": 8, + "height": 32, "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -138,13 +140,14 @@ exports[`renders list item with custom description 1`] = ` collapsable={false} style={ { - "backgroundColor": "rgba(232, 222, 248, 1)", - "borderColor": "transparent", + "alignItems": "center", + "backgroundColor": "rgba(254, 247, 255, 1)", + "borderColor": "rgba(121, 116, 126, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -177,6 +180,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]} @@ -194,10 +203,19 @@ exports[`renders list item with custom description 1`] = ` }, [ { - "borderRadius": 8, + "flexGrow": 1, + "flexShrink": 1, + "height": "100%", }, { - "width": "100%", + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "borderRadius": 8, }, ], ] @@ -206,116 +224,110 @@ exports[`renders list item with custom description 1`] = ` > - + + + - - file-pdf-box - - + } + > - DOCS.pdf + file-pdf-box + + DOCS.pdf +