UNPKG

@ouroboros/react-native-picker

Version:

A replacement Picker for React-Native that relies on no other packages

206 lines (205 loc) 6.58 kB
/** * React Native Select Picker * * Select component for picking one option out of many * * @author Chris Nasr <chris@ouroboroscoding.com> * @copyright Ouroboros Coding Inc. * @created 2022-09-18 */ // Ouroboros modules import clone from '@ouroboros/clone'; import { afindi, afindo } from '@ouroboros/tools'; // NPM modules import { useEffect, useRef, useState } from 'react'; import { Modal, Pressable, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; // Constants const DEFAULT_ITEM_HEIGHT = 40; const DEFAULT_FONT_SIZE = 14; /** * Picker * * Acts like an web based iOS HTML Select element * * @name Picker * @access public * @param {Object} props Properties passed to the component * @returns React.Component */ const Picker = (props) => { // State const [open, openSet] = useState(false); const [scroll, scrollSet] = useState(null); const [text, textSet] = useState(''); // Refs const scrollTimer = useRef(null); const styleObject = useRef(clone(baseStyles)); // Value effect useEffect(() => { const o = afindo(props.options, 'value', props.value); textSet(o ? o.text : ''); }, [props.value, props.options]); // Open effect useEffect(() => { // If we have a ref to the scroll view if (scroll) { // Get the index of the value const i = afindi(props.options, 'value', open); if (i > -1) { scrollTo(i); } } }, [open, scroll]); // Style effects useEffect(() => { // Copy the base styles const styles = clone(baseStyles); // Get height const height = props.itemHeight || DEFAULT_ITEM_HEIGHT; // Set the heights styles.item.height = height; styles.items.height = (height * 3); styles.overlay.height = (height * 3); styles.overlayFog.height = height; styles.overlaySelected.height = height; // Set font styles.itemText.fontSize = props.fontSize || DEFAULT_FONT_SIZE; // Set new style ref styleObject.current = styles; }, [props.itemHeight, props.fontSize]); // Called to set the new value function done() { // If the value changed, notify the parent if (open !== props.value) { props.onChanged(open); } // Hide the options openSet(false); } // Called when the options are scrolled function scrolled(ev) { // Calculate the index based on the current y const i = Math.round(ev.nativeEvent.contentOffset.y / (props.itemHeight || DEFAULT_ITEM_HEIGHT)); // Set timer for setting the new option if (scrollTimer.current) { clearTimeout(scrollTimer.current); } scrollTimer.current = setTimeout(() => { // If the value has actually changed, set it, that will take care // of scrolling directly on it if (props.options[i].value !== open) { openSet(props.options[i].value); } // Else, if the value hasn't changed, take care of re-centering the // current value else { scrollTo(i); } }, 100); } // Called to scroll the view to the index function scrollTo(i) { // Assuming we have the ref if (scroll) { // Scroll to the given item based on index and item height scroll.scrollTo({ x: 0, y: i * (props.itemHeight || DEFAULT_ITEM_HEIGHT), animated: true }); } } // Render component return (<> <Pressable onPress={() => openSet(props.value)}> {props.component ? <props.component text={text}/> : <View pointerEvents="none"> <TextInput caretHidden={true} style={props.style} textAlign={props.textAlign} value={text}/> </View>} </Pressable> {open !== false && <Modal animationType="fade" onRequestClose={() => openSet(false)} transparent={true} visible={true}> <View style={styleObject.current.modal}> <View style={styleObject.current.container}> <View style={styleObject.current.header}> <TouchableOpacity onPress={done} style={styleObject.current.done}> <Text style={styleObject.current.doneText}>Done</Text> </TouchableOpacity> </View> <ScrollView bounces={false} onScroll={scrolled} ref={scrollSet} scrollEventThrottle={0} style={styleObject.current.items}> <View style={styleObject.current.item}></View> {props.options.map(o => <Pressable key={o.value} onPress={() => openSet(o.value)}> <View style={styleObject.current.item}> <Text style={styleObject.current.itemText}>{o.text}</Text> </View> </Pressable>)} <View style={styleObject.current.item}></View> </ScrollView> <View style={styleObject.current.overlay} pointerEvents="none"> <View style={styleObject.current.overlayFog}></View> <View style={styleObject.current.overlaySelected}></View> <View style={styleObject.current.overlayFog}></View> </View> </View> </View> </Modal>} </>); }; export default Picker; const baseStyles = { container: { backgroundColor: '#caced6', borderColor: '#d5d5d7', borderWidth: 1, bottom: 0, position: 'absolute', width: '100%' }, done: {}, doneText: { color: '#5b87ba', paddingHorizontal: 12, paddingVertical: 8 }, header: { alignItems: 'flex-end', backgroundColor: '#ecedf0', flexGrow: 0, }, item: { height: DEFAULT_ITEM_HEIGHT, alignItems: 'center', justifyContent: 'center' }, items: { height: (DEFAULT_ITEM_HEIGHT * 3) }, itemText: { color: '#000000', fontSize: DEFAULT_FONT_SIZE }, modal: { alignItems: 'flex-end', backgroundColor: '#00000080', flex: 1 }, overlay: { backgroundColor: 'transparent', bottom: 0, height: (DEFAULT_ITEM_HEIGHT * 3), position: 'absolute', width: '100%', zIndex: 10 }, overlayFog: { backgroundColor: '#caced6d9', height: DEFAULT_ITEM_HEIGHT }, overlaySelected: { borderBottomWidth: 2, borderColor: '#b9bec6', borderTopWidth: 2, height: DEFAULT_ITEM_HEIGHT } };