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,
},
})
}