import { HeaderBackButton, HeaderButton, PlatformPressable } from '@react-navigation/elements' import { useNavigation } from '@react-navigation/native' import type { NativeStackHeaderItemProps, NativeStackNavigationOptions, } from '@react-navigation/native-stack' import { useLayoutEffect } from 'react' import { ColorValue, Platform, StyleSheet } from 'react-native' import type { SFSymbol } from 'sf-symbols-typescript' import { useFontScale, useTheme } from '../../hooks' import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils' import { Icon, type IconString } from './icon' import { TextButton } from './text_button' interface HeaderTextButtonProps { onPress: () => void title?: string disabled?: boolean } export const HeaderTextButton = ({ onPress, title = 'Submit', ...props }: HeaderTextButtonProps) => { const styles = useStyles() return ( {title} ) } interface HeaderDismissButtonProps { onPress: () => void title?: string tintColor?: ColorValue } export const HeaderDismissButton = ({ onPress, title = 'Cancel', tintColor, ...props }: HeaderDismissButtonProps) => { const styles = useStyles() return Platform.select({ ios: ( {title} ), default: ( ), }) } const createTextHeaderRight = ({ onPress, title, disabled }: HeaderTextButtonProps) => (props: NativeStackHeaderItemProps) => ( ) export type OptionsDecorator = ( options: NativeStackNavigationOptions ) => NativeStackNavigationOptions export const composeOptions = ( base: NativeStackNavigationOptions, ...decorators: OptionsDecorator[] ): NativeStackNavigationOptions => decorators.reduce((options, decorate) => decorate(options), base) interface TextButtonDecoratorProps { text: string onPress: () => void disabled?: boolean } export const withRightText = ({ text, onPress, disabled }: TextButtonDecoratorProps): OptionsDecorator => options => ({ ...options, headerRight: createTextHeaderRight({ onPress, title: text, disabled }), unstable_headerRightItems: () => [{ type: 'button' as const, label: text, onPress, disabled }], }) const HEADER_ICON_SIZE = 17 const HEADER_GLASS_BUTTON_SIZE = 44 const HEADER_BAR_INSET = 16 const HEADER_SLOT_GAP = 12 export const useHeaderSlotReserve = (): number => { const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER_LANDMARK }) return HEADER_BAR_INSET + HEADER_GLASS_BUTTON_SIZE * fontScale + HEADER_SLOT_GAP } const HEADER_FALLBACK_HIT_SLOP = 14 const iconStyles = StyleSheet.create({ disabled: { opacity: 0.4, }, fallbackButton: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 8, }, fallbackButtonLeft: { marginRight: Platform.select({ android: 16, default: 0 }), }, }) const SF_SYMBOL_NAMES: Partial> = { 'general.x': 'xmark', 'general.check': 'checkmark', 'general.upArrow': 'arrow.up', 'general.leftChevron': 'chevron.left', } const sfSymbolFor = (icon: IconString): SFSymbol => { const name = SF_SYMBOL_NAMES[icon] if (!name) throw new Error(`No SF Symbol mapped for icon "${icon}" — add one to SF_SYMBOL_NAMES.`) return name } export interface HeaderIconProps { icon: IconString accessibilityLabel: string onPress: () => void disabled?: boolean prominent?: boolean tintColor?: ColorValue } const createHeaderIconItem = ({ icon, accessibilityLabel, onPress, disabled, prominent, tintColor, }: HeaderIconProps) => ({ type: 'button' as const, label: '', accessibilityLabel, onPress, disabled, icon: { type: 'sfSymbol' as const, name: sfSymbolFor(icon) }, ...(prominent ? { variant: 'prominent' as const, tintColor } : {}), }) const createHeaderIconElement = (position: 'left' | 'right') => ({ icon, accessibilityLabel, onPress, disabled, tintColor }: HeaderIconProps) => () => ( ) export const withRightIcon = (props: HeaderIconProps): OptionsDecorator => options => ({ ...options, headerRight: createHeaderIconElement('right')(props), unstable_headerRightItems: () => [createHeaderIconItem(props)], }) export const withLeftIcon = (props: HeaderIconProps): OptionsDecorator => options => ({ ...options, headerLeft: createHeaderIconElement('left')(props), unstable_headerLeftItems: () => [createHeaderIconItem(props)], }) export const withRightClose = ({ onPress }: { onPress: () => void }) => withRightIcon({ icon: 'general.x', accessibilityLabel: 'Close', onPress }) export const withLeftClose = ({ onPress }: { onPress: () => void }) => withLeftIcon({ icon: 'general.x', accessibilityLabel: 'Close', onPress }) export const withRightDone = ({ onPress }: { onPress: () => void }) => withRightIcon({ icon: 'general.check', accessibilityLabel: 'Done', onPress }) const createMinimalHeaderBackElement = ({ onPress }: { onPress: () => void }) => (props: NativeStackHeaderItemProps) => ( ) export const withMinimalBack = ({ onPress }: { onPress: () => void }): OptionsDecorator => options => ({ ...options, headerLeft: createMinimalHeaderBackElement({ onPress }), unstable_headerLeftItems: () => [ createHeaderIconItem({ icon: 'general.leftChevron', accessibilityLabel: 'Back', onPress }), ], }) type UseHeaderRightOptions = { onPress: () => void disabled?: boolean enabled?: boolean } & ( | (Omit & { title?: never }) | { title: string; icon?: never; accessibilityLabel?: never; prominent?: never } ) export const useHeaderRight = (options: UseHeaderRightOptions) => { const navigation = useNavigation() const { colors } = useTheme() const { onPress, disabled, enabled = true } = options const icon = options.icon const label = options.icon !== undefined ? options.accessibilityLabel : options.title const prominent = options.icon !== undefined ? options.prominent : undefined useLayoutEffect(() => { if (!enabled) { navigation.setOptions({ headerRight: () => null, unstable_headerRightItems: undefined }) return } const decorate = icon ? withRightIcon({ icon, accessibilityLabel: label, onPress, disabled, prominent, tintColor: prominent ? colors.interaction : undefined, }) : withRightText({ text: label, onPress, disabled }) navigation.setOptions(decorate({})) }, [colors.interaction, disabled, enabled, icon, label, navigation, onPress, prominent]) } const useStyles = () => { const { colors } = useTheme() return StyleSheet.create({ interactionColor: { color: Platform.select({ ios: colors.iOSModalHeaderButtonTextColor, android: colors.androidModalHeaderButtonTextColor, }), }, androidCancelButton: { marginRight: 16, }, iosCancelButton: { fontWeight: 'normal', }, disabledColor: { color: colors.textColorDefaultDisabled, }, }) }