import React from 'react' import { Pressable, StyleSheet } from 'react-native' import type { PressableProps, GestureResponderEvent } from 'react-native' import { useTheme, useFontScale, useCreateAndroidRippleColor, useSelectedChipBackgroundColor, } from '../../hooks' import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK, platformFontWeightBold, platformPressedOpacityStyle, } from '../../utils' import { Haptic } from '../../utils/native_adapters/configuration' import { tokens } from '../../vendor/tapestry/tokens' import { Icon, IconString } from './icon' import { Text } from './text' // ================================= // ====== Constants ================ // ================================= // We are omitting `onLongPress` because we are instead supporting `accessibilityShowsLargeContentViewer`. // This is triggered by an `onLongPress` event when an iOS device is in Apple's accessible font scale range. // Triggering this prop will display an almost full screen iOS a11y button. type RestrictedPressableProps = Omit // ================================= // ====== Component ================ // ================================= export interface ToggleButtonProps extends RestrictedPressableProps { /** * Whether the toggle button is active/checked */ active: boolean /** * Required accessibility label for screen readers. * When title is provided, this provides additional context. * When title is omitted (icon-only mode), this is the primary label. */ accessibilityLabel: string /** * Specifies whether fonts should be scaled down automatically to fit given style constraints. */ adjustsFontSizeToFit?: boolean /** * Specifies whether fonts should scale to respect the device's text size accessibility settings. The default is true. */ allowFontScaling?: boolean /** * Generates an icon to the left of the button text */ iconNameLeft?: IconString /** * Generates an icon to the right of the button text */ iconNameRight?: IconString /** * Specifies the maximum size a font can reach when allowFontScaling is enabled. */ maxFontSizeMultiplier?: number /** * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0). */ minimumFontScale?: number /** * Renders as text within the button. * When omitted, button displays in icon-only mode. */ title?: string /** * Pressable container styles */ style?: PressableProps['style'] /** * Specifies whether haptic feedback should be enabled. */ hapticFeedback?: boolean } export function ToggleButton({ active, accessibilityLabel, adjustsFontSizeToFit = false, allowFontScaling = true, iconNameLeft, iconNameRight, maxFontSizeMultiplier = MAX_FONT_SIZE_MULTIPLIER_LANDMARK, minimumFontScale, title, style, hapticFeedback = true, onPress, ...props }: ToggleButtonProps) { const hasTitle = Boolean(title) const styles = useStyles({ active, maxFontSizeMultiplier }) const { colors } = useTheme() const overrideStyles = StyleSheet.flatten(style) // Ensures the pressed styles still get applied const baseRippleColor = active ? colors.interaction : colors.fillColorNeutral050Base const androidRippleColor = useCreateAndroidRippleColor({ color: baseRippleColor }) const handlePress = (event: GestureResponderEvent) => { if (hapticFeedback) Haptic.impactLight() onPress?.(event) } return ( ({ ...styles.pressable, ...(pressed ? platformPressedOpacityStyle : null), ...overrideStyles, })} accessibilityRole="togglebutton" accessibilityLabel={accessibilityLabel} accessibilityState={{ checked: active }} android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }} onPress={handlePress} accessibilityShowsLargeContentViewer accessibilityLargeContentTitle={accessibilityLabel} {...props} > {iconNameLeft && ( )} {hasTitle && ( {title} )} {iconNameRight && ( )} ) } // ================================= // ====== Styles =================== // ================================= const useStyles = ({ active, maxFontSizeMultiplier }: Partial) => { const { colors, button } = useTheme() const fontScale = useFontScale({ maxFontSizeMultiplier }) const activeBackgroundColor = useSelectedChipBackgroundColor() const borderColor = active ? (colors.borderColorChipSelected ?? colors.interaction) : colors.borderColorChip const fillColor = active ? activeBackgroundColor : colors.fillColorChip const color = active ? (colors.textColorChipSelected ?? colors.interaction) : colors.textColorChip return StyleSheet.create({ pressable: { borderRadius: button.borderRadius * fontScale, borderWidth: 1, borderColor, backgroundColor: fillColor, paddingHorizontal: 16 * fontScale, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', height: 32 * fontScale, gap: 6 * fontScale, overflow: 'hidden', }, text: { textAlign: 'center', textAlignVertical: 'center', includeFontPadding: false, fontWeight: platformFontWeightBold, color, }, icon: { fontSize: tokens.fontSizeSm * fontScale, color, }, }) }