Created
May 10, 2023 10:43
-
-
Save casprine/54e09df1af7c92b68ac7fce99f1bd5a5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { ComponentProps, FunctionComponent, memo, useMemo } from 'react' | |
import { View } from 'react-native' | |
import styled, { css } from 'styled-components/native' | |
import { ThemeColorName } from '../../../types' | |
import { invariant, normalize, useOnPress } from '../../../util' | |
import { | |
Avatar, | |
Button, | |
Icon, | |
IconButton, | |
MultiSelect, | |
Radio, | |
Stack, | |
Switch, | |
Text | |
} from '../../elements/' | |
import { ListLogo } from './ListLogo' | |
type Function = () => void | |
interface ListBaseProps { | |
title: string | |
subTitle?: string | |
hasDivider?: boolean | |
size?: 'default' | 'short' | |
hasHaptic?: boolean | |
onPress?: () => void | |
analyticEventName?: string | |
analyticEventOptions?: {} | |
titleColor?: ThemeColorName | |
} | |
interface TappableElement { | |
onPress: () => void | |
analyticEventName: string | |
analyticEventOptions?: {} | |
} | |
interface AvatarLeadingElementType { | |
leadingElementType: 'avatar' | |
leadingElementProps: Omit<ComponentProps<typeof Avatar>, 'size'> | |
} | |
interface IconLeadingElementType { | |
leadingElementType: 'icon' | |
leadingElementProps: Omit< | |
ComponentProps<typeof Icon>, | |
'size' | 'customSize' | |
> & { | |
withContainer?: boolean | |
} | |
} | |
interface LogoLeadingElementType { | |
leadingElementType: 'logo' | |
leadingElementProps: ComponentProps<typeof ListLogo> | |
} | |
interface NoLeadingElementType { | |
leadingElementType?: 'none' | |
} | |
interface ButtonTrailingElement extends TappableElement { | |
trailingElementType: 'button' | |
trailingElementProps: Omit< | |
ComponentProps<typeof Button>, | |
| 'size' | |
| 'onPress' | |
| 'analyticEventName' | |
| 'analyticEventOptions' | |
| 'hasHaptic' | |
> | |
} | |
interface IconTrailingElement extends TappableElement { | |
trailingElementType: 'icon' | |
trailingElementProps: Omit< | |
ComponentProps<typeof IconButton>, | |
| 'size' | |
| 'onPress' | |
| 'analyticEventName' | |
| 'analyticEventOptions' | |
| 'hasHaptic' | |
> | |
} | |
interface TextTrailingElement { | |
trailingElementType: 'value' | |
trailingElementProps: ComponentProps<typeof Text> | |
} | |
interface SingleValueIconTrailingElement extends TappableElement { | |
trailingElementType: 'singleValueIcon' | |
trailingElementProps: { | |
value: ComponentProps<typeof Text> | |
icon: Omit<ComponentProps<typeof Icon>, 'size'> | |
} | |
} | |
interface DoubleIconTrailingElement { | |
trailingElementType: 'doubleIcon' | |
trailingElementProps: { | |
firstIcon: Omit<ComponentProps<typeof Icon>, 'size' | 'customSize'> | |
secondIcon: Omit<ComponentProps<typeof Icon>, 'size' | 'customSize'> | |
} | |
} | |
interface DoubleValueIconTrailingElement extends TappableElement { | |
trailingElementType: 'doubleValueIcon' | |
trailingElementProps: { | |
firstValue: ComponentProps<typeof Text> | |
secondValue?: ComponentProps<typeof Text> | |
icon: Omit<ComponentProps<typeof Icon>, 'size' | 'customSize'> | |
} | |
} | |
interface SwitchTrailingElement extends TappableElement { | |
trailingElementType: 'switch' | |
trailingElementProps: Omit< | |
ComponentProps<typeof Switch>, | |
'onPress' | 'analyticEventName' | 'analyticEventOptions' | 'hasHaptic' | |
> | |
} | |
interface RadioTrailingElement extends TappableElement { | |
trailingElementType: 'radio' | |
trailingElementProps: Omit< | |
ComponentProps<typeof Radio>, | |
'onPress' | 'analyticEventName' | 'analyticEventOptions' | 'hasHaptic' | |
> | |
} | |
interface MultiSelectTrailingElement extends TappableElement { | |
trailingElementType: 'multiSelect' | |
trailingElementProps: Omit< | |
ComponentProps<typeof MultiSelect>, | |
'onPress' | 'analyticEventName' | 'analyticEventOptions' | 'hasHaptic' | |
> | |
} | |
interface NoTrailingElement { | |
trailingElementType?: 'none' | |
trailingElementProps?: null | {} | |
} | |
export type LeadingElement = | |
| AvatarLeadingElementType | |
| IconLeadingElementType | |
| LogoLeadingElementType | |
| NoLeadingElementType | |
type TrailingElement = | |
| NoTrailingElement | |
| MultiSelectTrailingElement | |
| RadioTrailingElement | |
| ButtonTrailingElement | |
| IconTrailingElement | |
| TextTrailingElement | |
| SingleValueIconTrailingElement | |
| DoubleIconTrailingElement | |
| DoubleValueIconTrailingElement | |
| SwitchTrailingElement | |
export type ListProps = ListBaseProps & LeadingElement & TrailingElement | |
const ListItemWrapper: FunctionComponent<ListProps> = props => { | |
const { | |
title, | |
subTitle, | |
hasDivider = true, | |
size = 'default', | |
onPress, | |
analyticEventName, | |
analyticEventOptions, | |
hasHaptic = false, | |
titleColor = 'textPrimary' | |
} = props | |
const handleContainerPress = useOnPress({ | |
onPress, | |
analyticEventName, | |
analyticEventOptions, | |
hasHaptic | |
}) | |
function generateLeadingElement() { | |
switch (props?.leadingElementType) { | |
case 'avatar': | |
return <Avatar {...props.leadingElementProps} /> | |
case 'icon': | |
return ( | |
<> | |
{props.leadingElementProps.withContainer ? ( | |
<LeadingIconContainer justifyContent="center" alignItems="center"> | |
<Icon {...props.leadingElementProps} /> | |
</LeadingIconContainer> | |
) : ( | |
<Icon {...props.leadingElementProps} customSize={32} /> | |
)} | |
</> | |
) | |
case 'logo': | |
return <ListLogo {...props.leadingElementProps} /> | |
default: | |
return null | |
} | |
} | |
function generateTrailingElement() { | |
switch (props.trailingElementType) { | |
case 'button': | |
return ( | |
<Button | |
size="tiny" | |
{...props.trailingElementProps} | |
onPress={props.onPress} | |
analyticEventName={props.analyticEventName} | |
analyticEventOptions={props.analyticEventOptions} | |
hasHaptic={hasHaptic} | |
/> | |
) | |
case 'icon': | |
return ( | |
<IconButton | |
size="medium" | |
{...props.trailingElementProps} | |
onPress={onPress as Function} | |
analyticEventName={props.analyticEventName} | |
analyticEventOptions={props.analyticEventOptions} | |
hasHaptic={hasHaptic} | |
/> | |
) | |
case 'value': | |
return <Text {...props.trailingElementProps} spacing="s0" /> | |
case 'singleValueIcon': | |
return ( | |
<Stack | |
flexOne={false} | |
spacing="s0" | |
alignItems="center" | |
flexDirection="row"> | |
<Text {...props.trailingElementProps.value} spacing="s0" /> | |
<Icon | |
{...props.trailingElementProps.icon} | |
size="medium" | |
style={{ marginLeft: normalize(8) }} | |
/> | |
</Stack> | |
) | |
case 'doubleValueIcon': | |
return ( | |
<Stack | |
flexOne={false} | |
spacing="s0" | |
alignItems="center" | |
flexDirection="row"> | |
<Stack spacing="s2" alignItems="flex-end"> | |
<Text {...props.trailingElementProps.firstValue} spacing="s0" /> | |
{props.trailingElementProps.secondValue && ( | |
<Text | |
{...props.trailingElementProps.secondValue} | |
spacing="s0" | |
/> | |
)} | |
</Stack> | |
<Icon | |
{...props.trailingElementProps.icon} | |
size="medium" | |
style={{ marginLeft: normalize(5) }} | |
/> | |
</Stack> | |
) | |
case 'doubleIcon': | |
return ( | |
<Stack spacing="s0" flexDirection="row" alignItems="center"> | |
<Icon {...props.trailingElementProps.firstIcon} size="medium" /> | |
<Icon | |
{...props.trailingElementProps.secondIcon} | |
size="medium" | |
style={{ marginLeft: normalize(8) }} | |
/> | |
</Stack> | |
) | |
case 'switch': | |
return ( | |
<Switch | |
{...props.trailingElementProps} | |
onPress={props.onPress} | |
analyticEventName={props.analyticEventName} | |
analyticEventOptions={props.analyticEventOptions} | |
hasHaptic={hasHaptic} | |
/> | |
) | |
case 'radio': | |
return ( | |
<Radio | |
{...props.trailingElementProps} | |
onPress={props.onPress} | |
analyticEventName={props.analyticEventName} | |
analyticEventOptions={props.analyticEventOptions} | |
hasHaptic={hasHaptic} | |
/> | |
) | |
case 'multiSelect': | |
return ( | |
<MultiSelect | |
{...props.trailingElementProps} | |
onPress={props.onPress} | |
analyticEventName={props.analyticEventName} | |
analyticEventOptions={props.analyticEventOptions} | |
hasHaptic={hasHaptic} | |
/> | |
) | |
case 'none': | |
return null | |
default: | |
return <Icon size="medium" name="chevron-right" /> | |
} | |
} | |
const spacing = useMemo(() => { | |
if (size === 'short' && !subTitle) { | |
return { | |
minHeight: normalize(40), | |
padding: normalize(2) | |
} | |
} | |
if (!subTitle) { | |
return { | |
minHeight: normalize(60), | |
padding: normalize(10) | |
} | |
} | |
if (size === 'default' && subTitle) { | |
return { | |
minHeight: normalize(46), | |
padding: normalize(16) | |
} | |
} | |
}, [size, subTitle]) | |
const tappableTargets = ['button', 'switch', 'radio', 'multiSelect', 'value'] | |
const disableContainerOnPress = useMemo(() => { | |
return tappableTargets.includes(props?.trailingElementType as string) | |
}, [props.trailingElementType, tappableTargets]) | |
const Component = () => ( | |
<Stack | |
fullWidth | |
style={{ | |
flexDirection: 'row' | |
}} | |
spacing="s0" | |
alignItems="center"> | |
{props.leadingElementType && ( | |
<Stack | |
spacing="s0" | |
alignItems="center" | |
justifyContent="center" | |
style={{ | |
marginRight: normalize(12), | |
width: normalize(40), | |
height: normalize(40) | |
}}> | |
{generateLeadingElement()} | |
</Stack> | |
)} | |
<Stack | |
flexDirection="row" | |
alignItems="center" | |
justifyContent="space-between" | |
spacing="s0"> | |
<Stack spacing="s0" style={{ flex: 1 }}> | |
<Text | |
color={titleColor} | |
type={subTitle ? 'listMedium' : 'listDefault'}> | |
{title} | |
</Text> | |
{subTitle && ( | |
<Text color="textSecondary" type="listSubTitle"> | |
{subTitle} | |
</Text> | |
)} | |
</Stack> | |
<View style={{ marginLeft: normalize(12) }}> | |
{generateTrailingElement()} | |
</View> | |
</Stack> | |
</Stack> | |
) | |
if (disableContainerOnPress) { | |
return ( | |
<> | |
<ListContainer spacing={spacing}> | |
<Component /> | |
</ListContainer> | |
{hasDivider && ( | |
<StyledDivider | |
spacing={spacing} | |
hasLeadingElement={Boolean(props.leadingElementType)} | |
/> | |
)} | |
</> | |
) | |
} | |
invariant(analyticEventName, 'analyticEventName prop is required') | |
invariant(onPress, 'onPress is required') | |
return ( | |
<> | |
<TouchableListContainer | |
spacing={spacing} | |
style={({ pressed }: { pressed: boolean }) => [ | |
{ | |
opacity: pressed ? 0.7 : 1 | |
} | |
]} | |
onPress={handleContainerPress}> | |
<Component /> | |
</TouchableListContainer> | |
{hasDivider && ( | |
<StyledDivider | |
spacing={spacing} | |
hasLeadingElement={Boolean(props.leadingElementType)} | |
/> | |
)} | |
</> | |
) | |
} | |
export const ListItem = memo(ListItemWrapper) | |
const ListContainer = styled.View<{ | |
spacing?: { minHeight?: number; padding?: number } | |
}>` | |
justify-content: center; | |
background-color: ${({ theme }) => theme.colors.bgBase}; | |
padding-horizontal: ${({ theme }) => theme.spacing.s24}; | |
${({ spacing }) => | |
spacing?.padding && | |
css` | |
padding-vertical: ${spacing?.padding}px; | |
`} | |
${({ spacing }) => | |
spacing?.minHeight && | |
css` | |
min-height: ${spacing?.minHeight}px; | |
`} | |
` | |
const TouchableListContainer = styled.Pressable<{ | |
spacing?: { minHeight?: number; padding?: number } | |
}>` | |
justify-content: center; | |
background-color: ${({ theme }) => theme.colors.bgBase}; | |
padding-horizontal: ${({ theme }) => theme.spacing.s24}; | |
${({ spacing }) => | |
spacing?.padding && | |
css` | |
padding-vertical: ${spacing?.padding}px; | |
`} | |
${({ spacing }) => | |
spacing?.minHeight && | |
css` | |
min-height: ${spacing?.minHeight}px; | |
`} | |
` | |
const StyledDivider = styled.View<{ | |
hasLeadingElement?: boolean | |
spacing?: { padding?: number } | |
}>` | |
height: 1px; | |
width: ${({ hasLeadingElement }) => (hasLeadingElement ? '80%' : '87.5%')}; | |
background-color: ${({ theme }) => theme.colors.border}; | |
margin-left: auto; | |
margin-right: ${({ hasLeadingElement }) => (hasLeadingElement ? 0 : 'auto')}; | |
` | |
const LeadingIconContainer = styled(Stack)` | |
width: ${() => normalize(36)}px; | |
height: ${() => normalize(36)}px; | |
border-radius: ${({ theme }) => theme.roundedCorners.rc8 / 2}px; | |
background-color: ${({ theme }) => theme.colors.bgLayerOne}; | |
` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment