Skip to content

Instantly share code, notes, and snippets.

@izakfilmalter
Created May 8, 2022 10:02
Show Gist options
  • Save izakfilmalter/122ca08897f999219d229495ff18d07d to your computer and use it in GitHub Desktop.
Save izakfilmalter/122ca08897f999219d229495ff18d07d to your computer and use it in GitHub Desktop.
Bottom Sheet Select
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}
/>
)
}
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