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" >