Skip to content

Instantly share code, notes, and snippets.

@lukebrandonfarrell
Created July 18, 2023 16:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukebrandonfarrell/534bd927d72c08efd731123b7e023046 to your computer and use it in GitHub Desktop.
Save lukebrandonfarrell/534bd927d72c08efd731123b7e023046 to your computer and use it in GitHub Desktop.
import React, { ReactElement } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Subtitle } from '../subtitle/subtitle';
import { defaultStyle } from '../../styles/theme.style';
interface DateSelectProps {
label: string | undefined;
handlePickerOpen: () => void;
testID?: string;
isPlaceholder?: boolean;
}
export const DropdownField = ({
handlePickerOpen,
testID,
label,
isPlaceholder,
}: DateSelectProps): ReactElement => {
return (
<TouchableOpacity
style={styles.selectContainer}
onPress={handlePickerOpen}
testID={testID}>
<Subtitle size="s2" opacity={!isPlaceholder ? 1 : 0.3}>
{label}
</Subtitle>
<Icon
name="arrow-drop-down"
size={20}
color={defaultStyle.colors.richPlum}
/>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
selectContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: defaultStyle.colors.white,
borderWidth: 0.25,
borderColor: defaultStyle.colors.borders['0.25'],
borderRadius: defaultStyle.borderRadius.xl,
paddingHorizontal: 15,
paddingVertical: 16,
},
});
import React, { useState, useEffect, ReactElement, useRef } from 'react';
import { Platform, ViewStyle, TextStyle, StyleSheet } from 'react-native';
import { Picker } from '@react-native-picker/picker';
import { ItemValue } from '@react-native-picker/picker/typings/Picker';
import { defaultStyle } from '../../styles/theme.style';
import { SelectFieldItemType } from '../select-field/select-field';
import { PickerSheetIOS } from '../../components/picker-sheet-ios';
interface PickerSheetProps {
visible: boolean;
selectedValue: string | number | null;
onChangeValue: (value: ItemValue) => void;
onClose: () => void;
options: SelectFieldItemType[];
containerStyle?: ViewStyle;
textStyle?: TextStyle;
placeholder?: string;
}
export const PickerSheet = ({
visible,
selectedValue,
onChangeValue,
onClose,
options,
placeholder = 'Select an option...',
containerStyle = {},
textStyle = {},
}: PickerSheetProps): ReactElement => {
/** State */
const [localSelectedValue, setLocalSelectedValue] =
useState<ItemValue | null>(selectedValue);
const pickerRef = useRef<Picker<ItemValue>>(null);
/** Effects */
useEffect(() => setLocalSelectedValue(selectedValue), [selectedValue]);
useEffect(() => {
if (Platform.OS === 'android') {
if (!visible) {
pickerRef.current?.blur();
} else {
pickerRef.current?.focus();
}
}
}, [visible]);
if (Platform.OS === 'ios') {
return (
<PickerSheetIOS
visible={visible}
containerStyle={containerStyle}
textStyle={textStyle}
onCancel={onClose}
onConfirm={onSelectIOS}
innerContainerStyle={{}}>
<Picker
style={styles.pickerStyle}
selectedValue={localSelectedValue ? localSelectedValue : undefined}
onValueChange={(value: string | number): void => {
setLocalSelectedValue(value);
}}>
{options.map(option => (
<Picker.Item
key={option.value}
value={option.value}
label={option.label}
/>
))}
</Picker>
</PickerSheetIOS>
);
} else {
return (
<Picker
ref={pickerRef}
selectedValue={selectedValue ? selectedValue : undefined}
onValueChange={(value: string | number): void => {
if (value !== null) {
onChangeValue(value);
}
}}
onBlur={(): void => onClose()}
// This effectively, hides the in-built picker
style={{
display: 'none',
}}
// For some reason 'transparent' doesn't work here, so we just set it to the same color as our standard background
dropdownIconColor={defaultStyle.colors.paper}>
<Picker.Item label={placeholder} value={null} />
{options.map(option => (
<Picker.Item
key={option.value}
label={option.label}
value={option.value}
style={styles.itemAndroidStyle}
/>
))}
</Picker>
);
}
/**
* Changes value of picker via callback
* and hides the component
*/
function onSelectIOS(): void {
// If local value is 'null' then the user picked the first value
// they didn't interact with the picker
const value = !localSelectedValue ? options[0]?.value : localSelectedValue;
if (value) {
// Updates the value for this controlled component
onChangeValue(value);
// We use this line to keep the local picker value
// in sync with the control value
setLocalSelectedValue(selectedValue);
}
onClose();
}
};
const styles = StyleSheet.create({
pickerStyle: {
alignSelf: 'stretch',
backgroundColor: defaultStyle.colors.paper,
},
itemAndroidStyle: {
color: defaultStyle.colors.black,
},
});
import React, { useState, forwardRef, useImperativeHandle } from 'react';
import { ItemValue } from '@react-native-picker/picker/typings/Picker';
import { Keyboard, StyleSheet, View } from 'react-native';
import { find } from 'lodash';
import { useField } from 'formik';
import { PickerSheet } from '../picker-sheet/picker-sheet';
import { Label, LabelProps } from '../label/label';
import { DropdownField } from '../dropdown-field/dropdown-field';
import { Errors } from '../errors/errors';
import {
useMarginStyles,
MarginProps,
} from '../../utils/use-margin-styles/use-margin-styles';
export interface SelectFieldProps extends MarginProps, LabelProps {
name: string;
label?: string;
placeholder: string;
options: SelectFieldItemType[];
testID?: string;
onChangeValue?: (value: ItemValue) => void;
onSubmitEditing?: () => void;
}
export interface SelectFieldItemType {
label?: string;
value: string | number;
}
export interface SelectFieldRef {
openPicker: () => void;
closePicker: () => void;
}
export const SelectField = forwardRef<SelectFieldRef, SelectFieldProps>(
(
{
name,
label,
labelDescription,
labelSuffix,
placeholder,
options,
testID,
onChangeValue,
onSubmitEditing,
...props
},
ref,
) => {
const [visible, setVisible] = useState(false);
const [field, meta, helpers] = useField(name);
const marginStyles = useMarginStyles(props);
const isPlaceholder = !field.value && placeholder;
const selectedOptionLabel = find(
options,
o => o.value == field.value,
)?.label;
const dropdownLabel = isPlaceholder ? placeholder : selectedOptionLabel;
/** Callbacks */
const onPickerClose = (): void => {
setVisible(false);
onSubmitEditing && onSubmitEditing();
};
/** Effects */
useImperativeHandle(ref, () => ({
openPicker,
closePicker: (): void => setVisible(false),
}));
return (
<View style={[styles.containerStyle, marginStyles]}>
<Label
label={label}
labelDescription={labelDescription}
labelSuffix={labelSuffix}
/>
<DropdownField
label={dropdownLabel}
handlePickerOpen={openPicker}
testID={testID}
isPlaceholder={field.value ? false : true}
/>
<PickerSheet
visible={visible}
selectedValue={field.value}
options={options}
onClose={onPickerClose}
onChangeValue={handleSelection}
placeholder={placeholder}
/>
{meta.touched && meta.error && <Errors errors={meta.error} />}
</View>
);
/**
* Handles the selection of an option.
*
* @param value - the value of the selected option.
*/
function handleSelection(value: ItemValue): void {
// Optionally call the onChangeValue prop.
if (onChangeValue) {
onChangeValue(value);
} else if (value) {
const valueAsString = value.toString();
helpers.setValue(valueAsString);
}
}
/**
* Opens the picker.
*/
function openPicker(): void {
Keyboard.dismiss();
setVisible(true);
}
},
);
const styles = StyleSheet.create({
containerStyle: {
width: '100%',
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment