Created
May 8, 2022 10:02
-
-
Save izakfilmalter/122ca08897f999219d229495ff18d07d to your computer and use it in GitHub Desktop.
Bottom Sheet Select
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 { ReactNativeStyle } from '@emotion/native' | |
import { isNotNil } from '@steepleinc/shared' | |
import { AvatarType } from 'Components/Avatar/Avatar' | |
import { Select, SelectOption } from 'Components/Select' | |
import { getFieldErrors } from 'Helpers/form' | |
import { useField, useFormikContext } from 'formik' | |
import { ReactElement } from 'react' | |
export type FormikSelectProps< | |
T extends object, | |
O extends SelectOption = SelectOption, | |
> = { | |
name: keyof T | |
required?: boolean | |
avatarType?: AvatarType | |
options: ReadonlyArray<O> | |
label?: string | |
placeholder?: string | |
style?: ReactNativeStyle | |
disabled?: boolean | |
} | |
export const FormikSelect = < | |
T extends object, | |
O extends SelectOption = SelectOption, | |
>( | |
props: FormikSelectProps<T, O>, | |
): ReactElement<FormikSelectProps<T, O>> => { | |
const { | |
options, | |
name, | |
placeholder = '', | |
required = false, | |
label, | |
style, | |
disabled = false, | |
} = props | |
const [{ value }, , { setValue }] = useField<string>( | |
name as Extract<T, string>, | |
) | |
const { touched, errors, submitCount } = useFormikContext<T>() | |
return ( | |
<Select<O> | |
{...getFieldErrors<T>({ | |
error: errors[name], | |
touched: touched[name], | |
submitCount, | |
})} | |
options={options} | |
value={value} | |
setValue={setValue} | |
disabled={disabled} | |
required={required} | |
label={label} | |
style={style} | |
placeholder={placeholder} | |
/> | |
) | |
} |
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 { css } from '@emotion/native' | |
import { | |
BottomSheetFlatList, | |
BottomSheetModal, | |
useBottomSheetDynamicSnapPoints, | |
} from '@gorhom/bottom-sheet' | |
import { isNotNil, O, pipe, RA } from '@steepleinc/shared' | |
import { HandleComponent } from 'Components/BottomSheetComponents' | |
import { Icon } from 'Components/Icons/Icon' | |
import { | |
inputBorderColor, | |
InputContainer, | |
InputErrors, | |
InputLabel, | |
inputStyles, | |
} from 'Components/Inputs/InputPrimitives' | |
import { ModalBackdrop } from 'Components/Modals/ModalBackdrop' | |
import { Colors, getColor } from 'Helpers/colors' | |
import { getRef } from 'Helpers/ref' | |
import { rem, ren } from 'Helpers/size' | |
import { Content1 } from 'Helpers/typography/raleway' | |
import { Pressable, useSx } from 'dripsy' | |
import { | |
FC, | |
ReactElement, | |
ReactNode, | |
useCallback, | |
useMemo, | |
useRef, | |
} from 'react' | |
import { | |
Dimensions, | |
ListRenderItem, | |
StyleProp, | |
View, | |
ViewStyle, | |
} from 'react-native' | |
import { useSafeAreaInsets } from 'react-native-safe-area-context' | |
const { height } = Dimensions.get('window') | |
export type SelectOption = { | |
Label: ReactNode | |
value: string | |
} | |
type SelectProps<T extends SelectOption> = { | |
options: ReadonlyArray<T> | |
value: string | |
style?: StyleProp<ViewStyle> | |
label?: string | |
required?: boolean | |
error?: unknown | |
disabled?: boolean | |
setValue: (value: string) => void | |
placeholder?: string | |
RenderSelectItem?: FC<SelectItemProps<T>> | |
} | |
export const Select = <T extends SelectOption>( | |
props: SelectProps<T>, | |
): ReactElement => { | |
const { | |
options, | |
value, | |
style, | |
label, | |
required = false, | |
error, | |
disabled = false, | |
placeholder = '', | |
setValue, | |
RenderSelectItem = SelectItem, | |
} = props | |
const hasError = isNotNil(error) | |
const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], []) | |
const sx = useSx() | |
const { | |
animatedHandleHeight, | |
animatedSnapPoints, | |
animatedContentHeight, | |
handleContentLayout, | |
} = useBottomSheetDynamicSnapPoints(initialSnapPoints) | |
const bottomSheetModalRef = useRef<BottomSheetModal>(null) | |
const inputRef = useRef<View>(null) | |
const insets = useSafeAreaInsets() | |
// callbacks | |
const handleDismissPress = useCallback(() => { | |
pipe( | |
inputRef, | |
getRef, | |
O.map((x) => { | |
x.setNativeProps({ | |
style: css` | |
border-color: ${getColor(inputBorderColor.initial)}; | |
`, | |
}) | |
}), | |
) | |
pipe( | |
bottomSheetModalRef, | |
getRef, | |
O.map((x) => x.dismiss()), | |
) | |
}, []) | |
const handlePresentPress = useCallback(() => { | |
pipe( | |
inputRef, | |
getRef, | |
O.map((x) => { | |
x.setNativeProps({ | |
style: css` | |
border-color: ${getColor(inputBorderColor.focused)}; | |
`, | |
}) | |
}), | |
) | |
pipe( | |
bottomSheetModalRef, | |
getRef, | |
O.map((x) => x.present()), | |
) | |
}, []) | |
const renderItem = useCallback<ListRenderItem<T>>( | |
({ item }) => ( | |
<RenderSelectItem<T> | |
item={item} | |
onSelect={() => { | |
setValue(item.value) | |
handleDismissPress() | |
}} | |
/> | |
), | |
[setValue], | |
) | |
const selectedValueOpt = useMemo( | |
() => | |
pipe( | |
options, | |
RA.findFirst((x) => x.value === value), | |
), | |
[options, value], | |
) | |
return ( | |
<InputContainer style={style}> | |
{isNotNil(label) ? ( | |
<InputLabel | |
hasError={hasError} | |
label={label} | |
required={required} | |
disabled={disabled} | |
/> | |
) : null} | |
<Pressable | |
ref={inputRef} | |
onPress={handlePresentPress} | |
style={[ | |
inputStyles({ hasError, disabled }), | |
sx({ | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
alignItems: 'center', | |
pr: ren(4), | |
}), | |
]} | |
> | |
<Content1 margin={`auto auto ${rem(4)} 0`}> | |
{pipe( | |
selectedValueOpt, | |
O.fold( | |
() => placeholder, | |
(x) => x.Label, | |
), | |
)} | |
</Content1> | |
<Icon | |
name={'menu-down'} | |
color={Colors.White} | |
style={sx({ ml: ren(8) })} | |
/> | |
</Pressable> | |
<InputErrors>{error as string}</InputErrors> | |
<BottomSheetModal | |
stackBehavior={'push'} | |
snapPoints={animatedSnapPoints} | |
handleHeight={animatedHandleHeight} | |
contentHeight={animatedContentHeight} | |
ref={bottomSheetModalRef} | |
handleComponent={HandleComponent} | |
backdropComponent={ModalBackdrop} | |
backgroundComponent={() => null} | |
topInset={insets.top} | |
// We do this so that we can clear the yellow border when you press the backdrop. | |
onDismiss={handleDismissPress} | |
> | |
<BottomSheetFlatList<T> | |
style={sx({ | |
maxHeight: height * 0.9, | |
backgroundColor: 'Black', | |
pb: insets.bottom + ren(16), | |
})} | |
data={options} | |
onLayout={handleContentLayout} | |
keyExtractor={(x) => x.value} | |
renderItem={renderItem} | |
/> | |
</BottomSheetModal> | |
</InputContainer> | |
) | |
} | |
type SelectItemProps<T extends SelectOption> = { | |
item: T | |
onSelect: () => void | |
} | |
const SelectItem = <T extends SelectOption>( | |
props: SelectItemProps<T>, | |
): ReactElement => { | |
const { item, onSelect } = props | |
const sx = useSx() | |
return ( | |
<Pressable | |
onPress={onSelect} | |
style={({ pressed }) => | |
sx({ | |
height: ren(56), | |
px: ren(24), | |
justifyContent: 'center', | |
backgroundColor: pressed ? 'EV1' : 'transparent', | |
}) | |
} | |
> | |
<Content1>{item.Label}</Content1> | |
</Pressable> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment