import color from 'color'; import { forwardRef, memo, useMemo } from 'react'; import { type GestureResponderEvent, type StyleProp, type TextStyle, type ViewProps, type ViewStyle, } from 'react-native'; import { resolveStateVariant } from '../../utils'; import { type IconType, Icon } from '../Icon'; import CrossFadeIcon from '../Icon/CrossFadeIcon'; import { StateLayer } from '../StateLayer'; import { Surface } from '../Surface'; import { type TouchableRippleProps, TouchableRipple } from '../TouchableRipple'; import { IconButtonVariant } from './types'; import { defaultStyles } from './utils'; import { useActionState } from '../../hooks/useActionState'; const whiteSpace = 12; export type Props = Omit & { /** * Icon to display. */ name: string; /** * Mode of the icon button. By default there is no specified mode - only pressable icon will be rendered. */ variant?: IconButtonVariant; /** * Whether icon button is selected. A selected button receives alternative combination of icon and container colors. */ selected?: boolean; /** * Size of the icon. */ size?: 'xs' | 'sm' | 'md' | 'lg' | number; /** * Type of the icon. Default is material * Should be a number or a Design Token */ type?: IconType; /** * Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch. */ disabled?: boolean; /** * Whether an icon change is animated. */ animated?: boolean; /** * Accessibility label for the button. This is read by the screen reader when the user taps the button. */ accessibilityLabel?: string; /** * Function to execute on press. */ onPress?: (e: GestureResponderEvent) => void; /** * backgroundColor and color will be extracted from here and act as buttonBackgroundColor and iconColor */ style?: StyleProp; iconStyle?: StyleProp; /** * color of the icon */ color?: string; /** * Style of the innerContainer */ innerContainerStyle?: ViewStyle; /** * Props for the state layer * */ stateLayerProps?: ViewProps; }; const emptyObject = {}; const IconButton = ( { name, size = 24, color: _iconColor, type, accessibilityLabel, disabled = false, onPress, selected = false, animated = false, variant = 'default', style, innerContainerStyle: innerContainerStyleProp = emptyObject, testID, stateLayerProps = emptyObject, iconStyle, ...rest }: Props, ref: any, ) => { const IconComponent = animated ? CrossFadeIcon : Icon; const { hovered, actionsRef } = useActionState({ ref, actionsToListen: ['hover'] }); const state = resolveStateVariant({ selectedAndDisabled: selected && disabled, selectedAndHovered: hovered && selected, disabled, hovered, selected, }); defaultStyles.useVariants({ variant, state, size: typeof size === 'string' && size ? size : undefined, }); const { iconColor, iconSize, rippleColor, containerStyle, accessibilityState, innerContainerStyle, // accessibilityTraits, stateLayerStyle, } = useMemo(() => { const iconSizeInNum = typeof size === 'number' && size ? size : undefined; let _rippleColor: string | undefined; try { _rippleColor = color(_iconColor).alpha(0.12).rgb().string(); } catch (e) { _rippleColor = undefined; } return { iconColor: _iconColor, iconSize: iconSizeInNum, rippleColor: _rippleColor, innerContainerStyle: [defaultStyles.innerContainer, innerContainerStyleProp], containerStyle: [ iconSizeInNum ? { width: iconSizeInNum + whiteSpace, height: iconSizeInNum + whiteSpace, } : {}, defaultStyles.root, style, ], // accessibilityTraits: disabled ? ['button', 'disabled'] : 'button', accessibilityState: { disabled }, stateLayerStyle: [defaultStyles.stateLayer, stateLayerProps?.style], }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ _iconColor, disabled, innerContainerStyleProp, size, stateLayerProps?.style, style, state, variant, ]); return ( <> ); }; const rippleSupportedHitSlop = { top: 10, left: 10, bottom: 10, right: 10 }; const rippleUnsupportedHitSlop = { top: 6, left: 6, bottom: 6, right: 6 }; export default memo(forwardRef(IconButton));