338 lines
9.1 KiB
TypeScript
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,
|
||
|
|
}
|