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 c59d02fa6e..d2f9d0a8fb 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -10,39 +10,44 @@ import type { ViewStyle, } from 'react-native'; -import useLatestCallback from 'use-latest-callback'; - import { getChipColors } from './helpers'; import type { ChipAvatarProps } from './helpers'; +import { ChipTokens } 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`. + */ + label?: string; + /** + * @deprecated Use `label` instead. Children are kept as a compatibility + * fallback and should be plain text. */ - children: React.ReactNode; + 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; /** @@ -55,13 +60,12 @@ 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 + * @deprecated Selected chips use the MD3 selected container and state layer. + * This prop is kept as a compatibility overlay. */ showSelectedOverlay?: boolean; /** @@ -74,7 +78,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; @@ -111,17 +115,16 @@ 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. + * @deprecated MD3 chips have a fixed density. This only reduces label-only + * horizontal padding for compatibility. */ 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>; @@ -138,7 +141,7 @@ export type Props = $Omit, 'mode'> & { */ testID?: string; /** - * Ellipsize Mode for the children text + * Ellipsize Mode for the label text. */ ellipsizeMode?: EllipsizeProp; /** @@ -164,14 +167,15 @@ export type Props = $Omit, 'mode'> & { * import { Chip } from 'react-native-paper'; * * const MyComponent = () => ( - * console.log('Pressed')}>Example Chip + * console.log('Pressed')} /> * ); * * export default MyComponent; * ``` */ const Chip = ({ - mode = 'flat', + mode = 'outlined', + label, children, icon, avatar, @@ -193,6 +197,7 @@ const Chip = ({ theme: themeOverrides, testID = 'chip', selectedColor, + showSelectedOverlay = false, showSelectedCheck = true, ellipsizeMode, compact, @@ -202,11 +207,8 @@ const Chip = ({ ...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 labelContent = label ?? children; const hasPassedTouchHandler = hasTouchHandler({ onPress, @@ -214,35 +216,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, @@ -252,43 +228,61 @@ const Chip = ({ borderColor, textColor, iconColor, + closeIconColor, contentOpacity, selectedBackgroundColor, backgroundColor, + rippleColor, + avatarOverlayColor, } = getChipColors({ isOutlined, + selected, + elevated, theme, selectedColor, customBackgroundColor, disabled, }); + const hasAvatar = !!avatar && !icon; + const showSelectedIcon = selected && showSelectedCheck && !icon; + const showLeadingIcon = !!icon || showSelectedIcon; + const hasLeading = hasAvatar || showLeadingIcon; + const hasClose = !!onClose; + const labelOnly = !hasLeading && !hasClose; + + const leftPadding = hasAvatar + ? ChipTokens.avatarLeadingPadding + : hasLeading + ? ChipTokens.iconLeadingPadding + : compact + ? ChipTokens.compactPadding + : ChipTokens.leadingPadding; + const rightPadding = hasClose + ? ChipTokens.closeTrailingPadding + : compact && labelOnly + ? ChipTokens.compactPadding + : ChipTokens.trailingPadding; + const touchTargetInset = + (ChipTokens.minimumTouchTarget - ChipTokens.containerHeight) / 2; + const touchTargetHitSlop = { + top: touchTargetInset, + bottom: touchTargetInset, + }; + const closeAndroidRipple = + Platform.OS === 'android' + ? { color: rippleColor, borderless: false } + : undefined; + const accessibilityState: AccessibilityState = { selected, - disabled, + disabled: isTouchableDisabled, }; - 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, - }; return ( - {avatar && !icon ? ( + {showSelectedOverlay && selected ? ( + pointerEvents="none" + style={[styles.selectedOverlay, { backgroundColor: rippleColor }]} + /> + ) : null} + {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} + {labelContent} - {onClose ? ( - - - - {closeIcon ? ( - - ) : ( - - )} - - - + {hasClose ? ( + [ + styles.closeButton, + Platform.OS === 'web' && pressed + ? { backgroundColor: rippleColor } + : null, + ]} + > + + ) : null} ); @@ -425,72 +409,64 @@ const Chip = ({ const styles = StyleSheet.create({ container: { - borderWidth: StyleSheet.hairlineWidth, + height: ChipTokens.containerHeight, + borderWidth: ChipTokens.outlineWidth, borderStyle: 'solid', - flexDirection: Platform.select({ default: 'column', web: 'row' }), + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'flex-start', + overflow: 'hidden', }, - md3Container: { - borderWidth: 1, + touchable: { + height: '100%', + 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, + selectedOverlay: { + ...StyleSheet.absoluteFill, }, - md3LabelText: { - textAlignVertical: 'center', - marginVertical: 6, + avatarWrapper: { + width: ChipTokens.avatarSize, + height: ChipTokens.avatarSize, + borderRadius: ChipTokens.avatarSize / 2, + marginRight: ChipTokens.leadingLabelGap, + overflow: 'hidden', }, avatar: { - width: 24, - height: 24, - borderRadius: 12, - }, - avatarWrapper: { - marginRight: 4, + width: ChipTokens.avatarSize, + height: ChipTokens.avatarSize, + borderRadius: ChipTokens.avatarSize / 2, }, - md3AvatarWrapper: { - marginLeft: 4, - marginRight: 0, + avatarSelectedOverlay: { + ...StyleSheet.absoluteFill, + alignItems: 'center', + justifyContent: 'center', }, - md3SelectedIcon: { - paddingLeft: 4, + leadingIcon: { + width: ChipTokens.leadingIconSize, + height: ChipTokens.leadingIconSize, + marginRight: ChipTokens.leadingLabelGap, + alignItems: 'center', + justifyContent: 'center', }, - // eslint-disable-next-line react-native/no-color-literals - avatarSelected: { - position: 'absolute', - top: 4, - left: 4, - backgroundColor: 'rgba(0, 0, 0, .29)', + labelText: { + textAlignVertical: 'center', + includeFontPadding: false, }, - closeButtonStyle: { - position: 'absolute', - right: 0, + closeButton: { + width: ChipTokens.trailingIconTouchTarget, height: '100%', - justifyContent: 'center', alignItems: 'center', + justifyContent: 'center', }, - touchable: { - width: '100%', + disabled: { + opacity: ChipTokens.disabledContent, }, }); diff --git a/src/components/Chip/helpers.tsx b/src/components/Chip/helpers.tsx index 4b0fdf9e06..ccc3d05673 100644 --- a/src/components/Chip/helpers.tsx +++ b/src/components/Chip/helpers.tsx @@ -2,10 +2,9 @@ import type { ColorValue, StyleProp, ViewStyle } from 'react-native'; import color from 'color'; +import { ChipTokens } from './tokens'; import { tokens } from '../../theme/tokens'; -import type { InternalTheme, Theme } from '../../types'; - -const md3 = (theme: InternalTheme) => theme as Theme; +import type { InternalTheme } from '../../types'; const stateOpacity = tokens.md.sys.state.opacity; @@ -16,182 +15,227 @@ export type ChipAvatarProps = { type BaseProps = { theme: InternalTheme; isOutlined: boolean; + selected?: boolean; disabled?: boolean; + elevated?: boolean; }; -const getBorderColor = ({ - theme, - isOutlined, - disabled, - selectedColor, -}: BaseProps & { backgroundColor: ColorValue; selectedColor?: ColorValue }) => { - const isSelectedColor = selectedColor !== undefined; - const { colors } = md3(theme); - - if (!isOutlined) { - // If the Chip mode is "flat", set border color to transparent - return 'transparent'; +const withAlpha = (value: ColorValue, alpha: number) => { + if (typeof value === 'string') { + return color(value).alpha(alpha).rgb().string(); } - if (disabled) { - return colors.surfaceContainer; - } + return value; +}; - 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; - } +const withStringAlpha = (value: ColorValue, alpha: number) => { + const alphaColor = withAlpha(value, alpha); - return colors.outlineVariant; + return typeof alphaColor === 'string' ? alphaColor : undefined; }; -const getTextColor = ({ +const getContainerColor = ({ theme, isOutlined, + selected, disabled, - selectedColor, + elevated, + customBackgroundColor, }: BaseProps & { - selectedColor?: ColorValue; + customBackgroundColor?: ColorValue; }) => { - const isSelectedColor = selectedColor !== undefined; - const { colors } = md3(theme); if (disabled) { - return colors.onSurface; + return isOutlined + ? 'transparent' + : withAlpha( + theme.colors[ChipTokens.disabledColor], + ChipTokens.disabledContainer + ); } - if (isSelectedColor) { - return selectedColor; + if (customBackgroundColor !== undefined) { + return customBackgroundColor; + } + + if (selected) { + return theme.colors[ChipTokens.selectedContainerColor]; } if (isOutlined) { - return colors.onSurfaceVariant; + return theme.colors[ChipTokens.outlinedContainerColor]; } - return colors.onSecondaryContainer; + return elevated + ? theme.colors[ChipTokens.elevatedContainerColor] + : theme.colors[ChipTokens.flatContainerColor]; }; -const getDefaultBackgroundColor = ({ +const getBorderColor = ({ theme, isOutlined, -}: Omit) => { - const { colors } = md3(theme); - if (isOutlined) { - return colors.surface; + selected, + disabled, + selectedColor, +}: BaseProps & { + selectedColor?: ColorValue; +}) => { + if (!isOutlined || selected) { + return 'transparent'; + } + + if (disabled) { + return withAlpha( + theme.colors[ChipTokens.disabledColor], + ChipTokens.disabledOutline + ); } - return colors.secondaryContainer; + if (selectedColor !== undefined) { + return withAlpha(selectedColor, 0.29); + } + + return theme.colors[ChipTokens.outlineColor]; }; -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[ChipTokens.disabledColor]; } - if (disabled) { - if (isOutlined) { - return 'transparent'; - } - return colors.surfaceContainerLow; + if (selectedColor !== undefined) { + return selectedColor; + } + + if (selected) { + return theme.colors[ChipTokens.selectedLabelColor]; } - return getDefaultBackgroundColor({ theme, isOutlined }); + return theme.colors[ChipTokens.labelColor]; }; -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[ChipTokens.disabledColor]; + } + + if (selectedColor !== undefined) { + return selectedColor; + } + + if (selected) { + return theme.colors[ChipTokens.selectedIconColor]; + } + + return theme.colors[ChipTokens.leadingIconColor]; }; -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[ChipTokens.disabledColor]; } - if (isSelectedColor) { + if (selectedColor !== undefined) { return selectedColor; } - if (isOutlined) { - return colors.onSurfaceVariant; + if (selected) { + return theme.colors[ChipTokens.selectedTrailingIconColor]; } - return colors.onSecondaryContainer; + return theme.colors[ChipTokens.trailingIconColor]; +}; + +const getStateLayerColor = ({ + theme, + selected, +}: Pick) => { + const colorRole = selected + ? ChipTokens.selectedStateLayerColor + : ChipTokens.stateLayerColor; + + return ( + withStringAlpha(theme.colors[colorRole], stateOpacity.pressed) ?? + theme.colors.stateLayerPressed + ); }; 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 + ? ChipTokens.disabledContent : stateOpacity.enabled; 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: getStateLayerColor({ theme, selected }), + avatarOverlayColor: withAlpha( + theme.colors.scrim, + ChipTokens.selectedAvatarOverlay + ), }; }; diff --git a/src/components/Chip/tokens.ts b/src/components/Chip/tokens.ts new file mode 100644 index 0000000000..26131a51a7 --- /dev/null +++ b/src/components/Chip/tokens.ts @@ -0,0 +1,60 @@ +import type { ColorRole, Elevation, TypescaleKey } from '../../theme/types'; + +/** + * MD3 Chip component tokens. + * @see https://m3.material.io/components/chips/specs + */ +const dimensions = { + containerHeight: 32, + minimumTouchTarget: 48, + outlineWidth: 1, + leadingIconSize: 18, + trailingIconSize: 18, + avatarSize: 24, + selectedIconSize: 18, + leadingPadding: 16, + trailingPadding: 16, + compactPadding: 12, + iconLeadingPadding: 8, + avatarLeadingPadding: 4, + closeTrailingPadding: 8, + leadingLabelGap: 8, + trailingIconTouchTarget: 32, + labelTypescale: 'labelLarge', +} as const satisfies Record; + +const colors = { + elevatedContainerColor: 'surfaceContainerLow', + flatContainerColor: 'surfaceContainerLow', + selectedContainerColor: 'secondaryContainer', + outlinedContainerColor: 'surface', + labelColor: 'onSurfaceVariant', + selectedLabelColor: 'onSecondaryContainer', + leadingIconColor: 'primary', + selectedIconColor: 'onSecondaryContainer', + trailingIconColor: 'onSurfaceVariant', + selectedTrailingIconColor: 'onSecondaryContainer', + outlineColor: 'outlineVariant', + disabledColor: 'onSurface', + stateLayerColor: 'onSurfaceVariant', + selectedStateLayerColor: 'onSecondaryContainer', +} as const satisfies Record; + +const opacity = { + disabledContainer: 0.12, + disabledContent: 0.38, + disabledOutline: 0.12, + selectedAvatarOverlay: 0.29, +} as const; + +const elevation = { + flat: 0, + elevated: 1, +} as const satisfies Record; + +export const ChipTokens = { + ...dimensions, + ...colors, + ...opacity, + ...elevation, +}; diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index 1c04130ebb..87b9c494d9 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -13,22 +13,24 @@ import { getChipColors } from '../Chip/helpers'; const stateOpacity = tokens.md.sys.state.opacity; it('renders chip with onPress', () => { - const tree = render( {}}>Example Chip).toJSON(); + const tree = render( + {}} /> + ).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders chip with icon', () => { - const tree = render(Example Chip).toJSON(); + const tree = render( + + ).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders chip with close button', () => { const tree = render( - {}}> - Example Chip - + {}} /> ).toJSON(); expect(tree).toMatchSnapshot(); @@ -36,9 +38,12 @@ it('renders chip with close button', () => { it('renders chip with custom close button', () => { const tree = render( - {}} closeIcon="arrow-down"> - Example Chip - + {}} + closeIcon="arrow-down" + /> ).toJSON(); expect(tree).toMatchSnapshot(); @@ -46,23 +51,21 @@ it('renders chip with custom close button', () => { it('renders outlined disabled chip', () => { const tree = render( - - Example Chip - + ).toJSON(); expect(tree).toMatchSnapshot(); }); it('renders selected chip', () => { - const tree = render(Example Chip).toJSON(); + const tree = render().toJSON(); expect(tree).toMatchSnapshot(); }); it('renders disabled chip if there is no touch handler passed', () => { const { getByTestId } = render( - Disabled chip + ); expect(getByTestId('disabled-chip').props.accessibilityState).toMatchObject({ @@ -72,9 +75,7 @@ it('renders disabled chip if there is no touch handler passed', () => { it('renders active chip if only onLongPress handler is passed', () => { const { getByTestId } = render( - {}} testID="active-chip"> - Active chip - + {}} testID="active-chip" /> ); expect(getByTestId('active-chip').props.accessibilityState).toMatchObject({ @@ -84,9 +85,11 @@ it('renders active chip if only onLongPress handler is passed', () => { it('renders chip with zero border radius', () => { const { getByTestId } = render( - - Active chip - + ); expect(getByTestId('active-chip')).toHaveStyle({ @@ -94,6 +97,15 @@ it('renders chip with zero border radius', () => { }); }); +it('uses label before deprecated children', () => { + const { getByText, queryByText } = render( + Children chip + ); + + expect(getByText('Label chip')).toBeTruthy(); + expect(queryByText('Children chip')).toBeNull(); +}); + describe('getChipColors - text color', () => { it('should return correct disabled color, for theme version 3', () => { expect( @@ -115,7 +127,7 @@ describe('getChipColors - text color', () => { isOutlined: false, }) ).toMatchObject({ - textColor: getTheme().colors.onSecondaryContainer, + textColor: getTheme().colors.onSurfaceVariant, }); }); @@ -164,7 +176,7 @@ describe('getChipColors - icon color', () => { isOutlined: false, }) ).toMatchObject({ - iconColor: getTheme().colors.onSecondaryContainer, + iconColor: getTheme().colors.primary, }); }); @@ -175,7 +187,7 @@ describe('getChipColors - icon color', () => { isOutlined: true, }) ).toMatchObject({ - iconColor: getTheme().colors.onSurfaceVariant, + iconColor: getTheme().colors.primary, }); }); @@ -222,6 +234,7 @@ describe('getChipColor - selected background color', () => { getChipColors({ theme: getTheme(), isOutlined: false, + selected: true, }) ).toMatchObject({ selectedBackgroundColor: getTheme().colors.secondaryContainer, @@ -260,7 +273,32 @@ describe('getChipColor - background color', () => { isOutlined: false, }) ).toMatchObject({ - backgroundColor: getTheme().colors.secondaryContainer, + backgroundColor: getTheme().colors.surfaceContainerLow, + }); + }); +}); + +describe('getChipColor - ripple color', () => { + it('falls back to precomputed state layer color for platform colors', () => { + const platformColor = { + resource_paths: ['@android:color/system_on_surface_variant_light'], + } as unknown as string; + const theme = { + ...getTheme(), + colors: { + ...getTheme().colors, + onSurfaceVariant: platformColor, + stateLayerPressed: 'rgba(29, 27, 32, 0.1)', + }, + }; + + expect( + getChipColors({ + theme, + isOutlined: true, + }) + ).toMatchObject({ + rippleColor: 'rgba(29, 27, 32, 0.1)', }); }); }); @@ -374,12 +412,11 @@ it('animated value changes correctly', () => { const value = new Animated.Value(1); const { getByTestId } = render( {}} testID="chip" style={[{ transform: [{ scale: value }] }]} - > - Example Chip - + /> ); expect(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 088be073fc..3ee49d728f 100644 --- a/src/components/__tests__/ListItem.test.tsx +++ b/src/components/__tests__/ListItem.test.tsx @@ -105,9 +105,7 @@ it('renders list item with custom description', () => { Design library that has you covered in all major use-cases. - {}}> - DOCS.pdf - + {}} /> )} diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap index 736f79b6fe..1b713433ee 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,15 @@ 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(202, 196, 208, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", + "overflow": "hidden", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -41,6 +45,7 @@ exports[`renders chip with close button 1`] = ` testID="chip-container" > - - + - - close - - - + }, + { + "backgroundColor": "transparent", + }, + ], + ] + } + > + close + @@ -303,8 +287,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 +306,15 @@ 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(202, 196, 208, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", + "overflow": "hidden", "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 +569,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 +588,15 @@ 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(202, 196, 208, 1)", "borderRadius": 8, "borderStyle": "solid", "borderWidth": 1, - "flex": undefined, - "flexDirection": "column", + "flex": 1, + "flexDirection": "row", + "overflow": "hidden", "shadowColor": "rgba(0, 0, 0, 1)", "shadowOffset": { "height": 0, @@ -637,6 +609,7 @@ exports[`renders chip with icon 1`] = ` testID="chip-container" >