|
| 1 | +import React, {useState, useCallback, useRef, useMemo} from 'react'; |
| 2 | +import {StyleSheet, TouchableWithoutFeedback, Keyboard as RNKeyboard} from 'react-native'; |
| 3 | +import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; |
| 4 | +import { |
| 5 | + Text, |
| 6 | + Spacings, |
| 7 | + NumberInput, |
| 8 | + NumberInputData, |
| 9 | + View, |
| 10 | + Typography, |
| 11 | + Constants, |
| 12 | + Incubator |
| 13 | +} from 'react-native-ui-lib'; |
| 14 | +import {renderBooleanOption, renderMultipleSegmentOptions} from '../ExampleScreenPresenter'; |
| 15 | + |
| 16 | +enum ExampleTypeEnum { |
| 17 | + PRICE = 'price', |
| 18 | + PERCENTAGE = 'percentage', |
| 19 | + ANY_NUMBER = 'number' |
| 20 | +} |
| 21 | + |
| 22 | +type ExampleType = ExampleTypeEnum | `${ExampleTypeEnum}`; |
| 23 | + |
| 24 | +const VALIDATION_MESSAGE = 'Please enter a valid number'; |
| 25 | +const MINIMUM_PRICE = 5000; |
| 26 | +const MINIMUM_PRICE_VALIDATION_MESSAGE = `Make sure your number is above ${MINIMUM_PRICE}`; |
| 27 | +const DISCOUNT_PERCENTAGE = {min: 1, max: 80}; |
| 28 | +// eslint-disable-next-line max-len |
| 29 | +const DISCOUNT_PERCENTAGE_VALIDATION_MESSAGE = `Make sure your number is between ${DISCOUNT_PERCENTAGE.min} and ${DISCOUNT_PERCENTAGE.max}`; |
| 30 | + |
| 31 | +const NumberInputScreen = () => { |
| 32 | + const currentData = useRef<NumberInputData>(); |
| 33 | + const [text, setText] = useState<string>(''); |
| 34 | + const [showLabel, setShowLabel] = useState<boolean>(true); |
| 35 | + const [exampleType, setExampleType] = useState<ExampleType>('price'); |
| 36 | + |
| 37 | + const processInput = useCallback(() => { |
| 38 | + let newText = ''; |
| 39 | + if (currentData.current) { |
| 40 | + switch (currentData.current.type) { |
| 41 | + case 'valid': |
| 42 | + newText = currentData.current.formattedNumber; |
| 43 | + break; |
| 44 | + case 'empty': |
| 45 | + newText = 'Empty'; |
| 46 | + break; |
| 47 | + case 'error': |
| 48 | + newText = `Error: value '${currentData.current.userInput}' is invalid`; |
| 49 | + break; |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + setText(newText); |
| 54 | + }, []); |
| 55 | + |
| 56 | + const onChangeNumber = useCallback((data: NumberInputData) => { |
| 57 | + currentData.current = data; |
| 58 | + processInput(); |
| 59 | + }, |
| 60 | + [processInput]); |
| 61 | + |
| 62 | + const label = useMemo(() => { |
| 63 | + if (showLabel) { |
| 64 | + switch (exampleType) { |
| 65 | + case 'price': |
| 66 | + default: |
| 67 | + return 'Enter price'; |
| 68 | + case 'percentage': |
| 69 | + return 'Enter discount percentage'; |
| 70 | + case 'number': |
| 71 | + return 'Enter any number'; |
| 72 | + } |
| 73 | + } |
| 74 | + }, [showLabel, exampleType]); |
| 75 | + |
| 76 | + const placeholder = useMemo(() => { |
| 77 | + switch (exampleType) { |
| 78 | + case 'price': |
| 79 | + default: |
| 80 | + return 'Price'; |
| 81 | + case 'percentage': |
| 82 | + return 'Discount'; |
| 83 | + case 'number': |
| 84 | + return 'Any number'; |
| 85 | + } |
| 86 | + }, [exampleType]); |
| 87 | + |
| 88 | + const fractionDigits = useMemo(() => { |
| 89 | + switch (exampleType) { |
| 90 | + case 'price': |
| 91 | + case 'number': |
| 92 | + default: |
| 93 | + return undefined; |
| 94 | + case 'percentage': |
| 95 | + return 0; |
| 96 | + } |
| 97 | + }, [exampleType]); |
| 98 | + |
| 99 | + const leadingText = useMemo(() => { |
| 100 | + switch (exampleType) { |
| 101 | + case 'price': |
| 102 | + return '$'; |
| 103 | + case 'percentage': |
| 104 | + case 'number': |
| 105 | + default: |
| 106 | + return undefined; |
| 107 | + } |
| 108 | + }, [exampleType]); |
| 109 | + |
| 110 | + const trailingText = useMemo(() => { |
| 111 | + switch (exampleType) { |
| 112 | + case 'percentage': |
| 113 | + return '%'; |
| 114 | + case 'price': |
| 115 | + case 'number': |
| 116 | + default: |
| 117 | + return undefined; |
| 118 | + } |
| 119 | + }, [exampleType]); |
| 120 | + |
| 121 | + const isValid = useCallback(() => { |
| 122 | + return currentData.current?.type === 'valid'; |
| 123 | + }, []); |
| 124 | + |
| 125 | + const isAboveMinimumPrice = useCallback(() => { |
| 126 | + if (currentData.current?.type === 'valid') { |
| 127 | + return currentData.current.number > MINIMUM_PRICE; |
| 128 | + } |
| 129 | + }, []); |
| 130 | + |
| 131 | + const isWithinDiscountPercentage = useCallback(() => { |
| 132 | + if (currentData.current?.type === 'valid') { |
| 133 | + return ( |
| 134 | + currentData.current.number >= DISCOUNT_PERCENTAGE.min && currentData.current.number <= DISCOUNT_PERCENTAGE.max |
| 135 | + ); |
| 136 | + } |
| 137 | + }, []); |
| 138 | + |
| 139 | + const validate = useMemo((): Incubator.TextFieldProps['validate'] => { |
| 140 | + switch (exampleType) { |
| 141 | + case 'price': |
| 142 | + return [isValid, isAboveMinimumPrice]; |
| 143 | + case 'percentage': |
| 144 | + return [isValid, isWithinDiscountPercentage]; |
| 145 | + default: |
| 146 | + return isValid; |
| 147 | + } |
| 148 | + }, [exampleType, isValid, isAboveMinimumPrice, isWithinDiscountPercentage]); |
| 149 | + |
| 150 | + const validationMessage = useMemo((): Incubator.TextFieldProps['validationMessage'] => { |
| 151 | + switch (exampleType) { |
| 152 | + case 'price': |
| 153 | + return [VALIDATION_MESSAGE, MINIMUM_PRICE_VALIDATION_MESSAGE]; |
| 154 | + case 'percentage': |
| 155 | + return [VALIDATION_MESSAGE, DISCOUNT_PERCENTAGE_VALIDATION_MESSAGE]; |
| 156 | + default: |
| 157 | + return VALIDATION_MESSAGE; |
| 158 | + } |
| 159 | + }, [exampleType]); |
| 160 | + |
| 161 | + return ( |
| 162 | + <TouchableWithoutFeedback onPress={RNKeyboard.dismiss}> |
| 163 | + <View flex centerH> |
| 164 | + <Text text40 margin-s10> |
| 165 | + Number Input |
| 166 | + </Text> |
| 167 | + {renderBooleanOption('Show label', 'showLabel', {spread: false, state: showLabel, setState: setShowLabel})} |
| 168 | + {renderMultipleSegmentOptions('', |
| 169 | + 'exampleType', |
| 170 | + [ |
| 171 | + {label: 'Price', value: ExampleTypeEnum.PRICE}, |
| 172 | + {label: 'Percentage', value: ExampleTypeEnum.PERCENTAGE}, |
| 173 | + {label: 'Number', value: ExampleTypeEnum.ANY_NUMBER} |
| 174 | + ], |
| 175 | + {state: exampleType, setState: setExampleType})} |
| 176 | + |
| 177 | + <View flex center> |
| 178 | + <NumberInput |
| 179 | + key={exampleType} |
| 180 | + // initialNumber={100} |
| 181 | + label={label} |
| 182 | + labelStyle={styles.label} |
| 183 | + placeholder={placeholder} |
| 184 | + fractionDigits={fractionDigits} |
| 185 | + onChangeNumber={onChangeNumber} |
| 186 | + leadingText={leadingText} |
| 187 | + leadingTextStyle={leadingText && [styles.infoText, {marginLeft: Spacings.s4}]} |
| 188 | + trailingText={trailingText} |
| 189 | + trailingTextStyle={trailingText && [styles.infoText, {marginRight: Spacings.s4}]} |
| 190 | + style={[ |
| 191 | + styles.mainText, |
| 192 | + !leadingText && {marginLeft: Spacings.s4}, |
| 193 | + !trailingText && {marginRight: Spacings.s4} |
| 194 | + ]} |
| 195 | + containerStyle={styles.containerStyle} |
| 196 | + validate={validate} |
| 197 | + validationMessage={validationMessage} |
| 198 | + validationMessageStyle={Typography.text80M} |
| 199 | + validateOnChange |
| 200 | + centered |
| 201 | + /> |
| 202 | + <Text marginT-s5>{text}</Text> |
| 203 | + </View> |
| 204 | + </View> |
| 205 | + </TouchableWithoutFeedback> |
| 206 | + ); |
| 207 | +}; |
| 208 | + |
| 209 | +export default gestureHandlerRootHOC(NumberInputScreen); |
| 210 | + |
| 211 | +const styles = StyleSheet.create({ |
| 212 | + containerStyle: { |
| 213 | + marginBottom: 30, |
| 214 | + marginLeft: Spacings.s5, |
| 215 | + marginRight: Spacings.s5 |
| 216 | + }, |
| 217 | + mainText: { |
| 218 | + height: 36, |
| 219 | + marginVertical: Spacings.s1, |
| 220 | + ...Typography.text30M |
| 221 | + }, |
| 222 | + infoText: { |
| 223 | + marginTop: Constants.isIOS ? Spacings.s2 : 0, |
| 224 | + ...Typography.text50M |
| 225 | + }, |
| 226 | + label: { |
| 227 | + marginBottom: Spacings.s1, |
| 228 | + ...Typography.text80M |
| 229 | + } |
| 230 | +}); |
0 commit comments