Files
2026-02-05 13:16:05 +08:00

258 lines
6.1 KiB
TypeScript

import { forwardRef, ReactElement } from "react"
import {
StyleProp,
TextStyle,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from "react-native"
import { useAppTheme } from "@/theme/context"
import { $styles } from "@/theme/styles"
import type { ThemedStyle } from "@/theme/types"
import { s } from "@/utils/responsive"
import { Icon, IconTypes } from "./Icon"
import { Text, TextProps } from "./Text"
export interface ListItemProps extends TouchableOpacityProps {
/**
* How tall the list item should be.
* Default: 56
*/
height?: number
/**
* Whether to show the top separator.
* Default: false
*/
topSeparator?: boolean
/**
* Whether to show the bottom separator.
* Default: false
*/
bottomSeparator?: boolean
/**
* Text to display if not using `tx` or nested components.
*/
text?: TextProps["text"]
/**
* Text which is looked up via i18n.
*/
tx?: TextProps["tx"]
/**
* Children components.
*/
children?: TextProps["children"]
/**
* Optional options to pass to i18n. Useful for interpolation
* as well as explicitly setting locale or translation fallbacks.
*/
txOptions?: TextProps["txOptions"]
/**
* Optional text style override.
*/
textStyle?: StyleProp<TextStyle>
/**
* Pass any additional props directly to the Text component.
*/
TextProps?: TextProps
/**
* Optional View container style override.
*/
containerStyle?: StyleProp<ViewStyle>
/**
* Optional TouchableOpacity style override.
*/
style?: StyleProp<ViewStyle>
/**
* Icon that should appear on the left.
*/
leftIcon?: IconTypes
/**
* An optional tint color for the left icon
*/
leftIconColor?: string
/**
* Icon that should appear on the right.
*/
rightIcon?: IconTypes
/**
* An optional tint color for the right icon
*/
rightIconColor?: string
/**
* Right action custom ReactElement.
* Overrides `rightIcon`.
*/
RightComponent?: ReactElement
/**
* Left action custom ReactElement.
* Overrides `leftIcon`.
*/
LeftComponent?: ReactElement
}
interface ListItemActionProps {
icon?: IconTypes
iconColor?: string
Component?: ReactElement
size: number
side: "left" | "right"
}
/**
* A styled row component that can be used in FlatList, SectionList, or by itself.
* @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/ListItem/}
* @param {ListItemProps} props - The props for the `ListItem` component.
* @returns {JSX.Element} The rendered `ListItem` component.
*/
export const ListItem = forwardRef<View, ListItemProps>(function ListItem(
props: ListItemProps,
ref,
) {
const {
bottomSeparator,
children,
height = s(56),
LeftComponent,
leftIcon,
leftIconColor,
RightComponent,
rightIcon,
rightIconColor,
style,
text,
TextProps,
topSeparator,
tx,
txOptions,
textStyle: $textStyleOverride,
containerStyle: $containerStyleOverride,
...TouchableOpacityProps
} = props
const { themed } = useAppTheme()
const isTouchable =
TouchableOpacityProps.onPress !== undefined ||
TouchableOpacityProps.onPressIn !== undefined ||
TouchableOpacityProps.onPressOut !== undefined ||
TouchableOpacityProps.onLongPress !== undefined
const $textStyles = [$textStyle, $textStyleOverride, TextProps?.style]
const $containerStyles = [
topSeparator && $separatorTop,
bottomSeparator && $separatorBottom,
$containerStyleOverride,
]
const $touchableStyles = [$styles.row, $touchableStyle, { minHeight: height }, style]
const Wrapper = isTouchable ? TouchableOpacity : View
return (
<View ref={ref} style={themed($containerStyles)}>
<Wrapper {...TouchableOpacityProps} style={$touchableStyles}>
<ListItemAction
side="left"
size={height}
icon={leftIcon}
iconColor={leftIconColor}
Component={LeftComponent}
/>
<Text {...TextProps} tx={tx} text={text} txOptions={txOptions} style={themed($textStyles)}>
{children}
</Text>
<ListItemAction
side="right"
size={height}
icon={rightIcon}
iconColor={rightIconColor}
Component={RightComponent}
/>
</Wrapper>
</View>
)
})
/**
* @param {ListItemActionProps} props - The props for the `ListItemAction` component.
* @returns {JSX.Element | null} The rendered `ListItemAction` component.
*/
function ListItemAction(props: ListItemActionProps) {
const { icon, Component, iconColor, size, side } = props
const { themed } = useAppTheme()
const $iconContainerStyles = [$iconContainer]
if (Component) {
return (
<View
style={themed([
$iconContainerStyles,
side === "left" && $iconContainerLeft,
side === "right" && $iconContainerRight,
{ height: size },
])}
>
{Component}
</View>
)
}
if (icon !== undefined) {
return (
<Icon
size={s(24)}
icon={icon}
color={iconColor}
containerStyle={themed([
$iconContainerStyles,
side === "left" && $iconContainerLeft,
side === "right" && $iconContainerRight,
{ height: size },
])}
/>
)
}
return null
}
const $separatorTop: ThemedStyle<ViewStyle> = ({ colors }) => ({
borderTopWidth: 1,
borderTopColor: colors.separator,
})
const $separatorBottom: ThemedStyle<ViewStyle> = ({ colors }) => ({
borderBottomWidth: 1,
borderBottomColor: colors.separator,
})
const $textStyle: ThemedStyle<TextStyle> = ({ spacing }) => ({
paddingVertical: spacing.xs,
alignSelf: "center",
flexGrow: 1,
flexShrink: 1,
})
const $touchableStyle: ViewStyle = {
alignItems: "flex-start",
}
const $iconContainer: ViewStyle = {
justifyContent: "center",
alignItems: "center",
flexGrow: 0,
}
const $iconContainerLeft: ThemedStyle<ViewStyle> = ({ spacing }) => ({
marginEnd: spacing.md,
})
const $iconContainerRight: ThemedStyle<ViewStyle> = ({ spacing }) => ({
marginStart: spacing.md,
})