import React from 'react' import { Pressable, StyleSheet, type StyleProp } from 'react-native' import type { PressableProps, ViewStyle } from 'react-native' import { useTheme, useFontScale, useCreateAndroidRippleColor } from '../../hooks' import { platformPressedOpacityStyle } from '../../utils' import { Icon } from './icon' import type { IconProps, IconString } from './icon' import { Spinner } from './spinner' import { getColorKey, useIconButtonColorOptionMap } from './utils/button_colors' import type { IconButtonAppearanceUnion } from './utils/button_colors' // ================================= // ====== Constants ================ // ================================= const SIZES = { xxxs: 'xxxs', xxs: 'xxs', xs: 'xs', sm: 'sm', md: 'md', lg: 'lg', xl: 'xl', xxl: 'xxl', xxxl: 'xxxl', } as const type SizeUnion = (typeof SIZES)[keyof typeof SIZES] type SizeStyle = Record< SizeUnion, { fontSize: number height: number } > type AndroidRadiusSize = Record< SizeUnion, { radius: number } > // ================================= // ====== Component ================ // ================================= interface IconButtonProps extends PressableProps { /** * Provides context to screen readers about what the button does. * This is required as Icons don't inherently provide non-visual context like text does. */ accessibilityLabel: string /** * Updates the icon's colors. Defaults to 'neutral'. */ appearance?: IconButtonAppearanceUnion /** * Styles the icon itself. * `fontSize` allows for a custom size that also responds to the device's text scale. * `color` changes the color of the icon. */ iconStyle?: IconProps['style'] /** * Disables the button and replaces its content with a spinner */ loading?: boolean /** * Specifies the maximum size a font can reach when allowFontScaling is enabled. */ maxFontSizeMultiplier?: number /** * Generates an icon from `@planningcenter/icons` */ name: IconString /** * Changes the overall size of the button and its contents */ size?: SizeUnion /** * Styles the `Pressable` wrapper */ style?: StyleProp } export function IconButton({ accessibilityLabel, appearance = 'neutral', disabled = false, iconStyle, loading, maxFontSizeMultiplier, name, size = 'md', style, ...props }: IconButtonProps) { const styles = useStyles({ appearance, disabled, loading, maxFontSizeMultiplier, size }) const colorOptionMap = useIconButtonColorOptionMap() const colorKey = getColorKey({ disabled, loading, appearance }) const { color = colorOptionMap[colorKey] } = StyleSheet.flatten(iconStyle || {}) const androidRippleColor = useCreateAndroidRippleColor({ color: color }) const androidRadiusSizeMap: AndroidRadiusSize = { [SIZES.xxxs]: { radius: 8, }, [SIZES.xxs]: { radius: 10, }, [SIZES.xs]: { radius: 12, }, [SIZES.sm]: { radius: 14, }, [SIZES.md]: { radius: 16, }, [SIZES.lg]: { radius: 18, }, [SIZES.xl]: { radius: 20, }, [SIZES.xxl]: { radius: 26, }, [SIZES.xxxl]: { radius: 32, }, } return ( [styles.pressable, style, pressed && platformPressedOpacityStyle]} accessibilityRole="button" disabled={disabled || loading} accessibilityState={{ busy: loading }} accessibilityLabel={accessibilityLabel} hitSlop={10} android_ripple={{ color: androidRippleColor, borderless: true, foreground: true, radius: androidRadiusSizeMap[size].radius, }} {...props} > {loading ? ( ) : ( )} ) } // ================================= // ====== Styles =================== // ================================= const useStyles = ({ appearance = 'neutral', disabled = false, loading = false, maxFontSizeMultiplier, size = 'md', }: Partial) => { const { colors } = useTheme() const fontScale = useFontScale({ maxFontSizeMultiplier }) const colorOptionMap = useIconButtonColorOptionMap() const colorKey = getColorKey({ disabled, loading, appearance }) const sizeStyleMap: SizeStyle = { [SIZES.xxxs]: { fontSize: 8, height: 8 * fontScale, }, [SIZES.xxs]: { fontSize: 10, height: 10 * fontScale, }, [SIZES.xs]: { fontSize: 12, height: 12 * fontScale, }, [SIZES.sm]: { fontSize: 14, height: 14 * fontScale, }, [SIZES.md]: { fontSize: 16, height: 16 * fontScale, }, [SIZES.lg]: { fontSize: 20, height: 20 * fontScale, }, [SIZES.xl]: { fontSize: 24, height: 24 * fontScale, }, [SIZES.xxl]: { fontSize: 32, height: 32 * fontScale, }, [SIZES.xxxl]: { fontSize: 40, height: 40 * fontScale, }, } return StyleSheet.create({ pressable: { alignItems: 'center', justifyContent: 'center', height: sizeStyleMap[size].height, }, icon: { fontSize: sizeStyleMap[size].fontSize, color: colorOptionMap[colorKey], }, iconDisabled: { color: colors.iconColorDefaultDisabled, }, spinner: { fontSize: sizeStyleMap[size].fontSize, }, }) }