Files
RN_Template/RN_TEMPLATE/app/components/Header.tsx
2026-02-05 13:16:05 +08:00

338 lines
9.1 KiB
TypeScript

import { ReactElement } from "react"
import {
StyleProp,
TextStyle,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from "react-native"
import { useTranslation } from "react-i18next"
import { isRTL } from "@/i18n"
import { useAppTheme } from "@/theme/context"
import { $styles } from "@/theme/styles"
import type { ThemedStyle } from "@/theme/types"
import { s } from "@/utils/responsive"
import { ExtendedEdge, useSafeAreaInsetsStyle } from "@/utils/useSafeAreaInsetsStyle"
import { IconTypes, PressableIcon } from "./Icon"
import { Text, TextProps } from "./Text"
export interface HeaderProps {
/**
* The layout of the title relative to the action components.
* - `center` will force the title to always be centered relative to the header. If the title or the action buttons are too long, the title will be cut off.
* - `flex` will attempt to center the title relative to the action buttons. If the action buttons are different widths, the title will be off-center relative to the header.
*/
titleMode?: "center" | "flex"
/**
* Optional title style override.
*/
titleStyle?: StyleProp<TextStyle>
/**
* Optional outer title container style override.
*/
titleContainerStyle?: StyleProp<ViewStyle>
/**
* Optional inner header wrapper style override.
*/
style?: StyleProp<ViewStyle>
/**
* Optional outer header container style override.
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Background color
*/
backgroundColor?: string
/**
* Title text to display if not using `tx` or nested components.
*/
title?: TextProps["text"]
/**
* Title text which is looked up via i18n.
*/
titleTx?: TextProps["tx"]
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
titleTxOptions?: TextProps["txOptions"]
/**
* Icon that should appear on the left.
* Can be used with `onLeftPress`.
*/
leftIcon?: IconTypes
/**
* An optional tint color for the left icon
*/
leftIconColor?: string
/**
* Left action text to display if not using `leftTx`.
* Can be used with `onLeftPress`. Overrides `leftIcon`.
*/
leftText?: TextProps["text"]
/**
* Left action text text which is looked up via i18n.
* Can be used with `onLeftPress`. Overrides `leftIcon`.
*/
leftTx?: TextProps["tx"]
/**
* Left action custom ReactElement if the built in action props don't suffice.
* Overrides `leftIcon`, `leftTx` and `leftText`.
*/
LeftActionComponent?: ReactElement
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
leftTxOptions?: TextProps["txOptions"]
/**
* What happens when you press the left icon or text action.
*/
onLeftPress?: TouchableOpacityProps["onPress"]
/**
* Icon that should appear on the right.
* Can be used with `onRightPress`.
*/
rightIcon?: IconTypes
/**
* An optional tint color for the right icon
*/
rightIconColor?: string
/**
* Right action text to display if not using `rightTx`.
* Can be used with `onRightPress`. Overrides `rightIcon`.
*/
rightText?: TextProps["text"]
/**
* Right action text text which is looked up via i18n.
* Can be used with `onRightPress`. Overrides `rightIcon`.
*/
rightTx?: TextProps["tx"]
/**
* Right action custom ReactElement if the built in action props don't suffice.
* Overrides `rightIcon`, `rightTx` and `rightText`.
*/
RightActionComponent?: ReactElement
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
rightTxOptions?: TextProps["txOptions"]
/**
* What happens when you press the right icon or text action.
*/
onRightPress?: TouchableOpacityProps["onPress"]
/**
* Override the default edges for the safe area.
*/
safeAreaEdges?: ExtendedEdge[]
}
interface HeaderActionProps {
backgroundColor?: string
icon?: IconTypes
iconColor?: string
text?: TextProps["text"]
tx?: TextProps["tx"]
txOptions?: TextProps["txOptions"]
onPress?: TouchableOpacityProps["onPress"]
ActionComponent?: ReactElement
}
/**
* Header that appears on many screens. Will hold navigation buttons and screen title.
* The Header is meant to be used with the `screenOptions.header` option on navigators, routes, or screen components via `navigation.setOptions({ header })`.
* @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Header/}
* @param {HeaderProps} props - The props for the `Header` component.
* @returns {JSX.Element} The rendered `Header` component.
*/
export function Header(props: HeaderProps) {
const {
theme: { colors },
themed,
} = useAppTheme()
const { t } = useTranslation()
const {
backgroundColor = colors.background,
LeftActionComponent,
leftIcon,
leftIconColor,
leftText,
leftTx,
leftTxOptions,
onLeftPress,
onRightPress,
RightActionComponent,
rightIcon,
rightIconColor,
rightText,
rightTx,
rightTxOptions,
safeAreaEdges = ["top"],
title,
titleMode = "center",
titleTx,
titleTxOptions,
titleContainerStyle: $titleContainerStyleOverride,
style: $styleOverride,
titleStyle: $titleStyleOverride,
containerStyle: $containerStyleOverride,
} = props
const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges)
const titleContent = titleTx ? t(titleTx, titleTxOptions) : title
return (
<View style={[$container, $containerInsets, { backgroundColor }, $containerStyleOverride]}>
<View style={[$styles.row, $wrapper, $styleOverride]}>
<HeaderAction
tx={leftTx}
text={leftText}
icon={leftIcon}
iconColor={leftIconColor}
onPress={onLeftPress}
txOptions={leftTxOptions}
backgroundColor={backgroundColor}
ActionComponent={LeftActionComponent}
/>
{!!titleContent && (
<View
style={[
$titleWrapperPointerEvents,
titleMode === "center" && themed($titleWrapperCenter),
titleMode === "flex" && $titleWrapperFlex,
$titleContainerStyleOverride,
]}
>
<Text
weight="medium"
size="md"
text={titleContent}
style={[$title, $titleStyleOverride]}
/>
</View>
)}
<HeaderAction
tx={rightTx}
text={rightText}
icon={rightIcon}
iconColor={rightIconColor}
onPress={onRightPress}
txOptions={rightTxOptions}
backgroundColor={backgroundColor}
ActionComponent={RightActionComponent}
/>
</View>
</View>
)
}
/**
* @param {HeaderActionProps} props - The props for the `HeaderAction` component.
* @returns {JSX.Element} The rendered `HeaderAction` component.
*/
function HeaderAction(props: HeaderActionProps) {
const { backgroundColor, icon, text, tx, txOptions, onPress, ActionComponent, iconColor } = props
const { themed } = useAppTheme()
const { t } = useTranslation()
const content = tx ? t(tx, txOptions) : text
if (ActionComponent) return ActionComponent
if (content) {
return (
<TouchableOpacity
style={themed([$actionTextContainer, { backgroundColor }])}
onPress={onPress}
disabled={!onPress}
activeOpacity={0.8}
>
<Text weight="medium" size="md" text={content} style={themed($actionText)} />
</TouchableOpacity>
)
}
if (icon) {
return (
<PressableIcon
size={s(24)}
icon={icon}
color={iconColor}
onPress={onPress}
containerStyle={[
themed([$actionIconContainer, { backgroundColor }]),
isRTL ? { transform: [{ rotate: "180deg" }] } : {},
]}
/>
)
}
return <View style={[$actionFillerContainer, { backgroundColor }]} />
}
const $wrapper: ViewStyle = {
height: s(56),
alignItems: "center",
justifyContent: "space-between",
}
const $container: ViewStyle = {
width: "100%",
}
const $title: TextStyle = {
textAlign: "center",
}
const $actionTextContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
flexGrow: 0,
alignItems: "center",
justifyContent: "center",
height: "100%",
paddingHorizontal: spacing.md,
zIndex: 2,
})
const $actionText: ThemedStyle<TextStyle> = ({ colors }) => ({
color: colors.tint,
})
const $actionIconContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
flexGrow: 0,
alignItems: "center",
justifyContent: "center",
height: "100%",
paddingHorizontal: spacing.md,
zIndex: 2,
})
const $actionFillerContainer: ViewStyle = {
width: s(16),
}
const $titleWrapperPointerEvents: ViewStyle = {
pointerEvents: "none",
}
const $titleWrapperCenter: ThemedStyle<ViewStyle> = ({ spacing }) => ({
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
position: "absolute",
paddingHorizontal: spacing.xxl,
zIndex: 1,
})
const $titleWrapperFlex: ViewStyle = {
justifyContent: "center",
flexGrow: 1,
}