import { ComponentType, forwardRef, memo, PropsWithoutRef, ReactNode, useContext, useMemo, } from 'react'; import { type GestureResponderEvent, type TextStyle, type ViewStyle, type TextProps, type ViewProps, View, StyleProp, } from 'react-native'; import { Text } from '../Text'; import type { MD3Elevation } from '../../types/theme'; import { useActionState } from '../../hooks'; import type { WithElements } from '../../types'; import { TouchableRipple, type TouchableRippleProps } from '../TouchableRipple'; import { IconButton, type IconButtonProps } from '../IconButton'; import { ActivityIndicator, type ActivityIndicatorProps } from '../ActivityIndicator'; import { Icon, type IconProps } from '../Icon'; import { BackgroundContext, resolveStateVariant } from '../../utils'; import { Surface } from '../Surface'; import { StateLayer } from '../StateLayer'; import { States, styles } from './utils'; export type Props = Omit & WithElements & { /** * label of the chip * Will be truncated if it's longer than 20 characters */ label: string; /** * character limit of the label */ labelCharacterLimit?: number; /** * Variant of the chip. * - `elevated` - elevated chip with shadow and without outline. * - `outlined` - chip with an outline. (outline will always have elevation 0 even if it's specified) */ variant?: 'elevated' | 'outlined'; /** * * */ size?: 'sm' | 'md'; /** * callback event when the closeIcon is pressed */ onClose?: () => void; /** * elevation level of the elevated chip */ elevation?: MD3Elevation; /** * selected state */ selected?: boolean; /** * props for the close icon * default is { name: 'close', onPress: onClose, disabled, accessibilityLabel: 'Close' } */ closeIconProps?: Partial; /** * props for the ActivityIndicator * default is { size: 18 } */ activityIndicatorProps?: Partial; /** * Whether to style the chip color as selected. */ selectedColor?: string; /** * Whether to display overlay on selected chip */ selectionBackgroundColor?: string; /** * Accessibility label for the chip. This is read by the screen reader when the user taps the chip. */ accessibilityLabel?: string; /** * Whether the chip is disabled. A disabled chip is greyed out and `onPress` is not called on touch. */ disabled?: boolean; /** * Function to execute on press. */ onPress?: (e: GestureResponderEvent) => void; /** * Whether to show the ActivityIndicator or not */ loading?: boolean; /** * container style */ containerStyle?: ViewStyle; /** * left element container style */ leftElementContainerStyle?: ViewStyle; /** * right element container style */ rightElementContainerStyle?: ViewStyle; /** * label style */ labelStyle?: Partial; /** * Pass down testID from chip props to touchable for Detox tests. */ /** * props for the stateLayer */ stateLayerProps?: PropsWithoutRef; testID?: string; containerProps?: Omit, 'style'>; leftElementIconProps?: IconProps; rightElementIconProps?: IconProps; backgroundColor?: string; invertLabelColor?: boolean; }; const Chip = ( { style, containerStyle: containerStyleProp, label: _label, labelCharacterLimit = 20, variant = 'outlined', size = 'md', disabled, elevation: elevationProp = 1, left, right, loading = false, onClose, closeIconProps, activityIndicatorProps, selected = false, leftElementContainerStyle, rightElementContainerStyle, labelStyle: labelStyleProp, accessibilityLabel, selectedColor, selectionBackgroundColor, stateLayerProps = {}, testID = 'chip', containerProps, leftElementIconProps, invertLabelColor, backgroundColor, ...rest }: Props, ref: any, ) => { const { hovered, actionsRef } = useActionState({ ref, actionsToListen: ['hover'] }); const state = resolveStateVariant({ disabled: !!disabled, selected, selectedAndHovered: selected && hovered, hovered, }); styles.useVariants({ variant, size, state: state as States, }); const iconSize = size === 'sm' ? 15 : 18; // const componentStyles = useComponentStyles('Chip', [ // style, // { // container: containerStyleProp || {}, // leftElement: leftElementContainerStyleProp || {}, // rightElement: rightElementContainerStyleProp || {}, // label: labelStyleProp || {}, // }, // selectionBackgroundColorProp // ? { selectionBackgroundColor: selectionBackgroundColorProp } // : {}, // selectedColorProp ? { selectedColor: selectedColorProp } : {}, // ]); const { containerStyle, touchableRippleStyle, leftElementStyle, rightElementStyle, labelStyle, stateLayerStyle, } = useMemo(() => { return { containerStyle: [ styles.container, selected && selectionBackgroundColor ? { backgroundColor: selectionBackgroundColor } : {}, containerStyleProp, ], touchableRippleStyle: [styles.touchableRippleContainer, style], leftElementStyle: [styles.leftElement, leftElementContainerStyle], rightElementStyle: [styles.rightElement, rightElementContainerStyle], labelStyle: [ styles.label, selected && selectedColor ? { color: selectedColor } : {}, labelStyleProp, ], stateLayerStyle: [styles.stateLayer, stateLayerProps?.style], }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ leftElementContainerStyle, rightElementContainerStyle, selected, selectedColor, selectionBackgroundColor, stateLayerProps?.style, style, containerStyleProp, labelStyleProp, state, size, variant, ]); const { accessibilityState, elevation } = useMemo( () => ({ accessibilityState: { selected, disabled, }, elevation: variant === 'outlined' || disabled ? 0 : elevationProp, }), [disabled, elevationProp, selected, variant], ); const label = `${_label}`.trim(); return ( <> ); }; type LeftElementProps = Pick & { leftElementStyle: StyleProp; iconSize: number; iconProps?: IconProps; invert?: boolean; }; const LeftElement = memo( ({ iconSize, loading, left, selected, activityIndicatorProps, leftElementStyle, iconProps, invert, }: LeftElementProps) => { return loading || left || selected ? ( {loading ? ( ) : ( left || ( ) )} ) : ( <> ); }, ); type RightElementProps = Pick< Props, 'onClose' | 'right' | 'closeIconProps' | 'disabled' | 'accessibilityState' > & { rightElementStyle: StyleProp; invert?: boolean; }; const RightElement = memo( ({ onClose, right, disabled, closeIconProps, rightElementStyle, accessibilityState, invert, }: RightElementProps) => { return onClose || right ? ( {onClose ? ( ) : ( right )} ) : ( <> ); }, ); const withInvertColorResolved = ( Component: ComponentType, prop: string = 'style', ) => ({ invert, ...props }: T & { invert?: boolean }) => { const { color: contrastColor } = useContext(BackgroundContext); const componentStyle = useMemo( () => (!invert ? props.style : [props.style, { color: contrastColor }]), [invert, props.style, contrastColor], ); const normalizedProps = { ...props, [prop]: componentStyle, } as unknown as T; return ; }; const Label = withInvertColorResolved((props: TextProps) => { return ; }); const IconWithContrastColor = withInvertColorResolved((props: IconProps) => { return ; }); const IconButtonWithContrastColor = withInvertColorResolved((props: IconButtonProps) => { return ; }, 'iconStyle'); export default memo(forwardRef(Chip));