From 2997f6878e19dac95b7599930a17db099e8181b1 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 31 Oct 2023 17:50:42 +0100 Subject: [PATCH 01/19] [TS migration] Migrate 'Picker' and 'LocalePicker' components --- src/components/LocalePicker.js | 66 ---- src/components/LocalePicker.tsx | 62 ++++ src/components/Picker/BasePicker.js | 299 ------------------ src/components/Picker/BasePicker.tsx | 210 ++++++++++++ .../{index.native.js => index.native.tsx} | 7 +- src/components/Picker/{index.js => index.tsx} | 9 +- src/components/Picker/types.ts | 96 ++++++ src/components/Text.tsx | 5 + 8 files changed, 378 insertions(+), 376 deletions(-) delete mode 100644 src/components/LocalePicker.js create mode 100644 src/components/LocalePicker.tsx delete mode 100644 src/components/Picker/BasePicker.js create mode 100644 src/components/Picker/BasePicker.tsx rename src/components/Picker/{index.native.js => index.native.tsx} (59%) rename src/components/Picker/{index.js => index.tsx} (73%) create mode 100644 src/components/Picker/types.ts diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js deleted file mode 100644 index 2c5a6e7b7ec6..000000000000 --- a/src/components/LocalePicker.js +++ /dev/null @@ -1,66 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import compose from '@libs/compose'; -import styles from '@styles/styles'; -import themeColors from '@styles/themes/default'; -import * as App from '@userActions/App'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import Picker from './Picker'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** Indicates which locale the user currently has selected */ - preferredLocale: PropTypes.string, - - /** Indicates size of a picker component and whether to render the label or not */ - size: PropTypes.oneOf(['normal', 'small']), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - preferredLocale: CONST.LOCALES.DEFAULT, - size: 'normal', -}; - -function LocalePicker(props) { - const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({ - value: language, - label: props.translate(`languagePage.languages.${language}.label`), - keyForList: language, - isSelected: props.preferredLocale === language, - })); - return ( - { - if (locale === props.preferredLocale) { - return; - } - - App.setLocale(locale); - }} - items={localesToLanguages} - size={props.size} - value={props.preferredLocale} - containerStyles={props.size === 'small' ? [styles.pickerContainerSmall] : []} - backgroundColor={themeColors.signInPage} - /> - ); -} - -LocalePicker.defaultProps = defaultProps; -LocalePicker.propTypes = propTypes; -LocalePicker.displayName = 'LocalePicker'; - -export default compose( - withLocalize, - withOnyx({ - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - }), -)(LocalePicker); diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx new file mode 100644 index 000000000000..62dd3123bbce --- /dev/null +++ b/src/components/LocalePicker.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import compose from '@libs/compose'; +import styles from '@styles/styles'; +import themeColors from '@styles/themes/default'; +import * as App from '@userActions/App'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Picker from './Picker'; +import type {PickerSize} from './Picker/types'; +import withLocalize from './withLocalize'; + +type LocalePickerOnyxProps = { + /** Indicates which locale the user currently has selected */ + preferredLocale: OnyxEntry>; +}; + +type LocalePickerProps = LocalePickerOnyxProps & { + /** Indicates size of a picker component and whether to render the label or not */ + size?: PickerSize; + + /** Returns translated string for given locale and phrase */ + translate: (phrase: string, variables?: Record) => string; +}; + +function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal', translate}: LocalePickerProps) { + const localesToLanguages = CONST.LANGUAGES.map((language) => ({ + value: language, + label: translate(`languagePage.languages.${language}.label`), + keyForList: language, + isSelected: preferredLocale === language, + })); + return ( + { + if (locale === preferredLocale || typeof locale !== 'string') { + return; + } + + App.setLocale(locale); + }} + items={localesToLanguages} + size={size} + value={preferredLocale} + containerStyles={size === 'small' ? [styles.pickerContainerSmall] : []} + backgroundColor={themeColors.signInPage} + /> + ); +} + +LocalePicker.displayName = 'LocalePicker'; + +export default compose( + withLocalize, + withOnyx({ + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + }), +)(LocalePicker); diff --git a/src/components/Picker/BasePicker.js b/src/components/Picker/BasePicker.js deleted file mode 100644 index 8e85eb66672a..000000000000 --- a/src/components/Picker/BasePicker.js +++ /dev/null @@ -1,299 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {View} from 'react-native'; -import RNPickerSelect from 'react-native-picker-select'; -import _ from 'underscore'; -import FormHelpMessage from '@components/FormHelpMessage'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import refPropTypes from '@components/refPropTypes'; -import {ScrollContext} from '@components/ScrollViewWithContext'; -import Text from '@components/Text'; -import styles from '@styles/styles'; -import themeColors from '@styles/themes/default'; - -const propTypes = { - /** A forwarded ref */ - forwardedRef: refPropTypes, - - /** BasePicker label */ - label: PropTypes.string, - - /** Should the picker appear disabled? */ - isDisabled: PropTypes.bool, - - /** Input value */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The items to display in the list of selections */ - items: PropTypes.arrayOf( - PropTypes.shape({ - /** The value of the item that is being selected */ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - - /** The text to display for the item */ - label: PropTypes.string.isRequired, - }), - ).isRequired, - - /** Something to show as the placeholder before something is selected */ - placeholder: PropTypes.shape({ - /** The value of the placeholder item, usually an empty string */ - value: PropTypes.string, - - /** The text to be displayed as the placeholder */ - label: PropTypes.string, - }), - - /** Error text to display */ - errorText: PropTypes.string, - - /** Customize the BasePicker container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the BasePicker background color */ - backgroundColor: PropTypes.string, - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - // eslint-disable-next-line react/no-unused-prop-types - shouldSaveDraft: PropTypes.bool, - - /** A callback method that is called when the value changes and it receives the selected value as an argument */ - onInputChange: PropTypes.func.isRequired, - - /** Size of a picker component */ - size: PropTypes.oneOf(['normal', 'small']), - - /** An icon to display with the picker */ - icon: PropTypes.func, - - /** Whether we should forward the focus/blur calls to the inner picker * */ - shouldFocusPicker: PropTypes.bool, - - /** Callback called when click or tap out of BasePicker */ - onBlur: PropTypes.func, - - /** Additional events passed to the core BasePicker for specific platforms such as web */ - additionalPickerEvents: PropTypes.func, - - /** Hint text that appears below the picker */ - hintText: PropTypes.string, -}; - -const defaultProps = { - forwardedRef: undefined, - label: '', - isDisabled: false, - errorText: '', - hintText: '', - containerStyles: [], - backgroundColor: undefined, - inputID: undefined, - shouldSaveDraft: false, - value: undefined, - placeholder: {}, - size: 'normal', - icon: (size) => ( - - ), - shouldFocusPicker: false, - onBlur: () => {}, - additionalPickerEvents: () => {}, -}; - -function BasePicker(props) { - const [isHighlighted, setIsHighlighted] = useState(false); - - // reference to the root View - const root = useRef(null); - - // reference to @react-native-picker/picker - const picker = useRef(null); - - // Windows will reuse the text color of the select for each one of the options - // so we might need to color accordingly so it doesn't blend with the background. - const placeholder = _.isEmpty(props.placeholder) - ? {} - : { - ...props.placeholder, - color: themeColors.pickerOptionsTextColor, - }; - - useEffect(() => { - if (props.value || !props.items || props.items.length !== 1 || !props.onInputChange) { - return; - } - - // When there is only 1 element in the selector, we do the user a favor and automatically select it for them - // so they don't have to spend extra time selecting the only possible value. - props.onInputChange(props.items[0].value, 0); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.items]); - - const context = useContext(ScrollContext); - - /** - * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onInputChange - * We are overriding this behavior to make BasePicker work with Form - * @param {String} value - * @param {Number} index - */ - const onInputChange = (value, index) => { - if (props.inputID) { - props.onInputChange(value); - return; - } - - props.onInputChange(value, index); - }; - - const enableHighlight = () => { - setIsHighlighted(true); - }; - - const disableHighlight = () => { - setIsHighlighted(false); - }; - - useImperativeHandle(props.forwardedRef, () => ({ - /** - * Focuses the picker (if configured to do so) - * - * This method is used by Form - */ - focus() { - if (!props.shouldFocusPicker) { - return; - } - - // Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the - // same task when we scrolled to it left that element in a glitched state, where the dropdown list can't - // be opened until the element gets re-focused - _.defer(() => { - picker.current.focus(); - }); - }, - - /** - * Like measure(), but measures the view relative to an ancestor - * - * This method is used by Form when scrolling to the input - * - * @param {Object} relativeToNativeComponentRef - reference to an ancestor - * @param {function(x: number, y: number, width: number, height: number): void} onSuccess - callback called on success - * @param {function(): void} onFail - callback called on failure - */ - measureLayout(relativeToNativeComponentRef, onSuccess, onFail) { - if (!root.current) { - return; - } - - root.current.measureLayout(relativeToNativeComponentRef, onSuccess, onFail); - }, - })); - - const hasError = !_.isEmpty(props.errorText); - - if (props.isDisabled) { - return ( - - {Boolean(props.label) && ( - - {props.label} - - )} - {props.value} - {Boolean(props.hintText) && {props.hintText}} - - ); - } - - return ( - <> - - {props.label && ( - - {props.label} - - )} - ({...item, color: themeColors.pickerOptionsTextColor}))} - style={props.size === 'normal' ? styles.picker(props.isDisabled, props.backgroundColor) : styles.pickerSmall(props.backgroundColor)} - useNativeAndroidPickerStyle={false} - placeholder={placeholder} - value={props.value} - Icon={() => props.icon(props.size)} - disabled={props.isDisabled} - fixAndroidTouchableBug - onOpen={enableHighlight} - onClose={disableHighlight} - textInputProps={{ - allowFontScaling: false, - }} - pickerProps={{ - ref: picker, - tabIndex: -1, - onFocus: enableHighlight, - onBlur: () => { - disableHighlight(); - props.onBlur(); - }, - ...props.additionalPickerEvents(enableHighlight, (value, index) => { - onInputChange(value, index); - disableHighlight(); - }), - }} - scrollViewRef={context && context.scrollViewRef} - scrollViewContentOffsetY={context && context.contentOffsetY} - /> - - - {Boolean(props.hintText) && {props.hintText}} - - ); -} - -BasePicker.propTypes = propTypes; -BasePicker.defaultProps = defaultProps; -BasePicker.displayName = 'BasePicker'; - -const BasePickerWithRef = React.forwardRef((props, ref) => ( - -)); - -BasePickerWithRef.displayName = 'BasePickerWithRef'; - -export default BasePickerWithRef; diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx new file mode 100644 index 000000000000..96521fd11bbe --- /dev/null +++ b/src/components/Picker/BasePicker.tsx @@ -0,0 +1,210 @@ +import {Picker} from '@react-native-picker/picker'; +import lodashDefer from 'lodash/defer'; +import React, {useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {View} from 'react-native'; +import RNPickerSelect from 'react-native-picker-select'; +import FormHelpMessage from '@components/FormHelpMessage'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {ScrollContext} from '@components/ScrollViewWithContext'; +import Text from '@components/Text'; +import styles from '@styles/styles'; +import themeColors from '@styles/themes/default'; +import type {BasePickerHandle, BasePickerProps} from './types'; + +function BasePicker({ + items, + forwardedRef, + backgroundColor, + inputID, + value, + onInputChange, + label = '', + isDisabled = false, + errorText = '', + hintText = '', + containerStyles = [], + placeholder = {}, + size = 'normal', + icon = (iconSize) => ( + + ), + shouldFocusPicker = false, + onBlur = () => {}, + additionalPickerEvents = () => {}, +}: BasePickerProps) { + const [isHighlighted, setIsHighlighted] = useState(false); + + // reference to the root View + const root = useRef(null); + + // reference to @react-native-picker/picker + const picker = useRef>(null); + + // Windows will reuse the text color of the select for each one of the options + // so we might need to color accordingly so it doesn't blend with the background. + const pickerPlaceholder = Object.keys(placeholder).length ? {...placeholder, color: themeColors.pickerOptionsTextColor} : {}; + + useEffect(() => { + if (!!value || !items || items.length !== 1 || !onInputChange) { + return; + } + + // When there is only 1 element in the selector, we do the user a favor and automatically select it for them + // so they don't have to spend extra time selecting the only possible value. + onInputChange(items[0].value, 0); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items]); + + const context = useContext(ScrollContext); + + /** + * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onValueChange + * We are overriding this behavior to make BasePicker work with Form + */ + const onValueChange = (inputValue: string, index: number) => { + if (inputID) { + onInputChange(inputValue); + return; + } + + onInputChange(inputValue, index); + }; + + const enableHighlight = () => { + setIsHighlighted(true); + }; + + const disableHighlight = () => { + setIsHighlighted(false); + }; + + useImperativeHandle(forwardedRef, () => ({ + /** + * Focuses the picker (if configured to do so) + * + * This method is used by Form + */ + focus() { + if (!shouldFocusPicker) { + return; + } + + // Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the + // same task when we scrolled to it left that element in a glitched state, where the dropdown list can't + // be opened until the element gets re-focused + lodashDefer(() => { + picker.current?.focus(); + }); + }, + + /** + * Like measure(), but measures the view relative to an ancestor + * + * This method is used by Form when scrolling to the input + * + * @param relativeToNativeComponentRef - reference to an ancestor + * @param onSuccess - callback called on success + * @param onFail - callback called on failure + */ + measureLayout(relativeToNativeComponentRef, onSuccess, onFail) { + if (!root.current) { + return; + } + + root.current.measureLayout(relativeToNativeComponentRef, onSuccess, onFail); + }, + })); + + const hasError = !!errorText; + + if (isDisabled) { + return ( + + {Boolean(label) && ( + + {label} + + )} + {value} + {Boolean(hintText) && {hintText}} + + ); + } + + return ( + <> + + {label && ( + + {label} + + )} + ({...item, color: themeColors.pickerOptionsTextColor}))} + style={size === 'normal' ? styles.picker(isDisabled, backgroundColor) : styles.pickerSmall(backgroundColor)} + useNativeAndroidPickerStyle={false} + placeholder={pickerPlaceholder} + value={value} + // TODO: update the icon logic to escape ts error + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + Icon={() => icon(size)} + disabled={isDisabled} + fixAndroidTouchableBug + onOpen={enableHighlight} + onClose={disableHighlight} + textInputProps={{ + allowFontScaling: false, + }} + pickerProps={{ + // TODO: resolve 'ref does not exist in type CustomPickerProps' ts error + // ref: picker, + tabIndex: -1, + onFocus: enableHighlight, + onBlur: () => { + disableHighlight(); + onBlur(); + }, + ...additionalPickerEvents(enableHighlight, (inputValue, index) => { + onValueChange(inputValue, index); + disableHighlight(); + }), + }} + scrollViewRef={context?.scrollViewRef} + scrollViewContentOffsetY={context?.contentOffsetY} + /> + + + {Boolean(hintText) && {hintText}} + + ); +} + +BasePicker.displayName = 'BasePicker'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/Picker/index.native.js b/src/components/Picker/index.native.tsx similarity index 59% rename from src/components/Picker/index.native.js rename to src/components/Picker/index.native.tsx index f441609fd4d0..f974130a291a 100644 --- a/src/components/Picker/index.native.js +++ b/src/components/Picker/index.native.tsx @@ -1,14 +1,11 @@ import React, {forwardRef} from 'react'; import BasePicker from './BasePicker'; +import {BasePickerHandle, BasePickerProps} from './types'; -const BasePickerWithRef = forwardRef((props, ref) => ( +export default forwardRef((props, ref) => ( )); - -BasePickerWithRef.displayName = 'BasePickerWithRef'; - -export default BasePickerWithRef; diff --git a/src/components/Picker/index.js b/src/components/Picker/index.tsx similarity index 73% rename from src/components/Picker/index.js rename to src/components/Picker/index.tsx index 8e49a42e8932..e3504bf4842c 100644 --- a/src/components/Picker/index.js +++ b/src/components/Picker/index.tsx @@ -1,7 +1,8 @@ import React, {forwardRef} from 'react'; import BasePicker from './BasePicker'; +import type {AdditionalPickerEvents, BasePickerHandle, BasePickerProps, OnChange, OnMouseDown} from './types'; -const additionalPickerEvents = (onMouseDown, onChange) => ({ +const additionalPickerEvents = (onMouseDown: OnMouseDown, onChange: OnChange): AdditionalPickerEvents => ({ onMouseDown, onChange: (e) => { if (e.target.selectedIndex === undefined) { @@ -13,7 +14,7 @@ const additionalPickerEvents = (onMouseDown, onChange) => ({ }, }); -const BasePickerWithRef = forwardRef((props, ref) => ( +export default forwardRef((props, ref) => ( ( additionalPickerEvents={additionalPickerEvents} /> )); - -BasePickerWithRef.displayName = 'BasePickerWithRef'; - -export default BasePickerWithRef; diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts new file mode 100644 index 000000000000..53f45f23a0ca --- /dev/null +++ b/src/components/Picker/types.ts @@ -0,0 +1,96 @@ +import {ChangeEvent, Component, ForwardedRef, ReactElement} from 'react'; +import {MeasureLayoutOnSuccessCallback, NativeMethods, ViewStyle} from 'react-native'; + +type MeasureLayoutOnFailCallback = () => void; + +type RelativeToNativeComponentRef = (Component & Readonly) | number; + +type BasePickerHandle = { + focus: () => void; + measureLayout: (relativeToNativeComponentRef: RelativeToNativeComponentRef, onSuccess: MeasureLayoutOnSuccessCallback, onFail: MeasureLayoutOnFailCallback) => void; +}; + +type OnMouseDown = () => void; + +type OnChange = (value: string, index: number) => void; + +type AdditionalPickerEvents = { + onMouseDown?: OnMouseDown; + onChange?: (event: ChangeEvent) => void; +}; + +type AdditionalPickerEventsCallback = (onMouseDown: OnMouseDown, onChange: OnChange) => AdditionalPickerEvents; + +type DefaultPickerEventsCallback = () => void; + +type PickerSize = 'normal' | 'small'; + +type PickerItem = { + /** The value of the item that is being selected */ + value: string | number; + + /** The text to display for the item */ + label: string; +}; + +type PickerPlaceholder = { + /** The value of the placeholder item, usually an empty string */ + value?: string; + + /** The text to be displayed as the placeholder */ + label?: string; +}; + +type BasePickerProps = { + forwardedRef?: ForwardedRef; + + /** BasePicker label */ + label?: string | null; + + /** Should the picker appear disabled? */ + isDisabled?: boolean; + + /** Input value */ + value?: string | null | number; + + /** The items to display in the list of selections */ + items: PickerItem[]; + + /** Something to show as the placeholder before something is selected */ + placeholder?: PickerPlaceholder; + + /** Error text to display */ + errorText?: string; + + /** Customize the BasePicker container */ + containerStyles?: ViewStyle[]; + + /** Customize the BasePicker background color */ + backgroundColor?: string; + + /** The ID used to uniquely identify the input in a Form */ + inputID?: string; + + /** A callback method that is called when the value changes and it receives the selected value as an argument */ + onInputChange: (value: string | number, index?: number) => void; + + /** Size of a picker component */ + size?: PickerSize; + + /** An icon to display with the picker */ + icon?: (size: PickerSize) => ReactElement; + + /** Whether we should forward the focus/blur calls to the inner picker * */ + shouldFocusPicker?: boolean; + + /** Callback called when click or tap out of BasePicker */ + onBlur?: () => void; + + /** Additional events passed to the core BasePicker for specific platforms such as web */ + additionalPickerEvents?: AdditionalPickerEventsCallback | DefaultPickerEventsCallback; + + /** Hint text that appears below the picker */ + hintText?: string; +}; + +export type {BasePickerHandle, BasePickerProps, AdditionalPickerEventsCallback, PickerSize, AdditionalPickerEvents, OnMouseDown, OnChange}; diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 80181156ee3a..a0aa90cb502d 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -12,13 +12,18 @@ type TextProps = RNTextProps & { /** The size of the text */ fontSize?: number; + /** The alignment of the text */ textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify'; + /** Any children to display */ children: React.ReactNode; /** The family of the font to use */ family?: keyof typeof fontFamily; + + /** Pointer events property to the Text element */ + pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto'; }; function Text( From 25f87b641f7b80deb4b86db6e94200ca9cb1b5b3 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 31 Oct 2023 18:00:24 +0100 Subject: [PATCH 02/19] Update picker style type --- src/components/LocalePicker.tsx | 2 +- src/components/Picker/BasePicker.tsx | 2 +- src/components/Picker/types.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 62dd3123bbce..a531674003bd 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -44,7 +44,7 @@ function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal', items={localesToLanguages} size={size} value={preferredLocale} - containerStyles={size === 'small' ? [styles.pickerContainerSmall] : []} + containerStyles={size === 'small' ? styles.pickerContainerSmall : {}} backgroundColor={themeColors.signInPage} /> ); diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 96521fd11bbe..956d3a08e6d9 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -144,7 +144,7 @@ function BasePicker({ <> {label && ( void; @@ -63,7 +63,7 @@ type BasePickerProps = { errorText?: string; /** Customize the BasePicker container */ - containerStyles?: ViewStyle[]; + containerStyles?: StyleProp; /** Customize the BasePicker background color */ backgroundColor?: string; From 80816b6bbf91fb1f12b419645673423b85b1fe35 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 2 Nov 2023 19:43:29 +0300 Subject: [PATCH 03/19] Update TODO messages --- src/components/Picker/BasePicker.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 956d3a08e6d9..3b49c727e83e 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -162,7 +162,7 @@ function BasePicker({ useNativeAndroidPickerStyle={false} placeholder={pickerPlaceholder} value={value} - // TODO: update the icon logic to escape ts error + // TODO: update the icon type in the lib to escape ts error // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Icon={() => icon(size)} @@ -187,6 +187,7 @@ function BasePicker({ disableHighlight(); }), }} + // TODO: for some reasons scrollViewRef and scrollViewContentOffsetY are not found in RNPickerSelect props, need to do updates on the lib side scrollViewRef={context?.scrollViewRef} scrollViewContentOffsetY={context?.contentOffsetY} /> From 4a1773e33150510175886cd0c40f6e20d9c79554 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 2 Nov 2023 20:17:51 +0300 Subject: [PATCH 04/19] Update react-native-picker-select lib version to resolve ts issues --- package-lock.json | 14 +++++++------- package.json | 2 +- src/components/Picker/BasePicker.tsx | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 690e658fa476..28946fa3e429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -44896,9 +44896,9 @@ } }, "node_modules/react-native-picker-select": { - "version": "8.0.4", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", - "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==", + "version": "8.1.0", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", + "integrity": "sha512-3QMmP/fjkq6+AaNXR09SCGajcbC11lVgtIvpuLoJHyI66Btj9/CSe19T7nvSVL4q2mQLKrGtpfwuhYIREzXWgw==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -85483,9 +85483,9 @@ "requires": {} }, "react-native-picker-select": { - "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", - "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==", - "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", + "integrity": "sha512-3QMmP/fjkq6+AaNXR09SCGajcbC11lVgtIvpuLoJHyI66Btj9/CSe19T7nvSVL4q2mQLKrGtpfwuhYIREzXWgw==", + "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", "requires": { "lodash.isequal": "^4.5.0" } diff --git a/package.json b/package.json index a8143fd2a809..6133f0386709 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 3b49c727e83e..e510ac485c33 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -187,7 +187,6 @@ function BasePicker({ disableHighlight(); }), }} - // TODO: for some reasons scrollViewRef and scrollViewContentOffsetY are not found in RNPickerSelect props, need to do updates on the lib side scrollViewRef={context?.scrollViewRef} scrollViewContentOffsetY={context?.contentOffsetY} /> From a8b8188970d8d35dba7c53001136be8419e6be3f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 6 Nov 2023 14:19:04 +0100 Subject: [PATCH 05/19] Update react-native-picker-select lib to fix TS issues --- package-lock.json | 12 ++++++------ package.json | 2 +- src/components/Picker/BasePicker.tsx | 9 ++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 778dd98b648d..409bf4cfc82e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,7 +101,7 @@ "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -44896,8 +44896,8 @@ }, "node_modules/react-native-picker-select": { "version": "8.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", - "integrity": "sha512-3QMmP/fjkq6+AaNXR09SCGajcbC11lVgtIvpuLoJHyI66Btj9/CSe19T7nvSVL4q2mQLKrGtpfwuhYIREzXWgw==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -85460,9 +85460,9 @@ "requires": {} }, "react-native-picker-select": { - "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", - "integrity": "sha512-3QMmP/fjkq6+AaNXR09SCGajcbC11lVgtIvpuLoJHyI66Btj9/CSe19T7nvSVL4q2mQLKrGtpfwuhYIREzXWgw==", - "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", + "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", + "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "requires": { "lodash.isequal": "^4.5.0" } diff --git a/package.json b/package.json index 94b94019abff..fa70af341e25 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#83c7f0ea8ee90932b57a7d9beb85ecb93d5ef03c", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index e510ac485c33..41fd16319813 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,4 +1,3 @@ -import {Picker} from '@react-native-picker/picker'; import lodashDefer from 'lodash/defer'; import React, {useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -43,7 +42,7 @@ function BasePicker({ const root = useRef(null); // reference to @react-native-picker/picker - const picker = useRef>(null); + const picker = useRef(null); // Windows will reuse the text color of the select for each one of the options // so we might need to color accordingly so it doesn't blend with the background. @@ -162,9 +161,6 @@ function BasePicker({ useNativeAndroidPickerStyle={false} placeholder={pickerPlaceholder} value={value} - // TODO: update the icon type in the lib to escape ts error - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Icon={() => icon(size)} disabled={isDisabled} fixAndroidTouchableBug @@ -174,8 +170,7 @@ function BasePicker({ allowFontScaling: false, }} pickerProps={{ - // TODO: resolve 'ref does not exist in type CustomPickerProps' ts error - // ref: picker, + ref: picker, tabIndex: -1, onFocus: enableHighlight, onBlur: () => { From e3769180841e2b1d8eb6d0172207d5831a9d67f9 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 9 Nov 2023 12:00:59 +0100 Subject: [PATCH 06/19] Update forwardRef logic --- src/components/Picker/BasePicker.tsx | 81 +++++++++++--------------- src/components/Picker/index.native.tsx | 20 ++++--- src/components/Picker/index.tsx | 32 +++++----- src/components/Picker/types.ts | 4 +- 4 files changed, 65 insertions(+), 72 deletions(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 41fd16319813..aba14d3b74a2 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,5 +1,5 @@ import lodashDefer from 'lodash/defer'; -import React, {useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -11,31 +11,33 @@ import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; import type {BasePickerHandle, BasePickerProps} from './types'; -function BasePicker({ - items, - forwardedRef, - backgroundColor, - inputID, - value, - onInputChange, - label = '', - isDisabled = false, - errorText = '', - hintText = '', - containerStyles = [], - placeholder = {}, - size = 'normal', - icon = (iconSize) => ( - - ), - shouldFocusPicker = false, - onBlur = () => {}, - additionalPickerEvents = () => {}, -}: BasePickerProps) { +function BasePicker( + { + items, + backgroundColor, + inputID, + value, + onInputChange, + label = '', + isDisabled = false, + errorText = '', + hintText = '', + containerStyles = [], + placeholder = {}, + size = 'normal', + icon = (iconSize) => ( + + ), + shouldFocusPicker = false, + onBlur = () => {}, + additionalPickerEvents = () => {}, + }: BasePickerProps, + ref: ForwardedRef, +) { const [isHighlighted, setIsHighlighted] = useState(false); // reference to the root View @@ -83,7 +85,7 @@ function BasePicker({ setIsHighlighted(false); }; - useImperativeHandle(forwardedRef, () => ({ + useImperativeHandle(ref, () => ({ /** * Focuses the picker (if configured to do so) * @@ -124,7 +126,7 @@ function BasePicker({ if (isDisabled) { return ( - + {Boolean(label) && ( + - {label && ( - - {label} - - )} + {label && {label}} {Boolean(hintText) && {hintText}} - + ); } BasePicker.displayName = 'BasePicker'; -export default React.forwardRef((props, ref) => ( - -)); +export default forwardRef(BasePicker); diff --git a/src/components/Picker/index.native.tsx b/src/components/Picker/index.native.tsx index f974130a291a..e0b8335215b4 100644 --- a/src/components/Picker/index.native.tsx +++ b/src/components/Picker/index.native.tsx @@ -1,11 +1,15 @@ -import React, {forwardRef} from 'react'; +import React, {ForwardedRef, forwardRef} from 'react'; import BasePicker from './BasePicker'; import {BasePickerHandle, BasePickerProps} from './types'; -export default forwardRef((props, ref) => ( - -)); +function Picker(props: BasePickerProps, ref: ForwardedRef) { + return ( + + ); +} + +export default forwardRef(Picker); diff --git a/src/components/Picker/index.tsx b/src/components/Picker/index.tsx index e3504bf4842c..cbd09d0c0b45 100644 --- a/src/components/Picker/index.tsx +++ b/src/components/Picker/index.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef} from 'react'; +import React, {ForwardedRef, forwardRef} from 'react'; import BasePicker from './BasePicker'; import type {AdditionalPickerEvents, BasePickerHandle, BasePickerProps, OnChange, OnMouseDown} from './types'; @@ -14,16 +14,20 @@ const additionalPickerEvents = (onMouseDown: OnMouseDown, onChange: OnChange): A }, }); -export default forwardRef((props, ref) => ( - -)); +function Picker(props: BasePickerProps, ref: ForwardedRef) { + return ( + + ); +} + +export default forwardRef(Picker); diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 56ffc16e8b39..003b076ac922 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -1,4 +1,4 @@ -import {ChangeEvent, Component, ForwardedRef, ReactElement} from 'react'; +import {ChangeEvent, Component, ReactElement} from 'react'; import {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; type MeasureLayoutOnFailCallback = () => void; @@ -42,8 +42,6 @@ type PickerPlaceholder = { }; type BasePickerProps = { - forwardedRef?: ForwardedRef; - /** BasePicker label */ label?: string | null; From f17b26ef61b4dfed2befc2b83eeca16af6f111c6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 17 Nov 2023 13:20:08 +0100 Subject: [PATCH 07/19] Updates after merging main --- src/components/LocalePicker.tsx | 26 +++++++++++++++----------- src/components/Picker/BasePicker.tsx | 6 +++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index a531674003bd..164c1688591a 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -2,11 +2,12 @@ import React from 'react'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import compose from '@libs/compose'; -import styles from '@styles/styles'; -import themeColors from '@styles/themes/default'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {LocaleContextProps} from './LocaleContextProvider'; import Picker from './Picker'; import type {PickerSize} from './Picker/types'; import withLocalize from './withLocalize'; @@ -16,15 +17,15 @@ type LocalePickerOnyxProps = { preferredLocale: OnyxEntry>; }; -type LocalePickerProps = LocalePickerOnyxProps & { - /** Indicates size of a picker component and whether to render the label or not */ - size?: PickerSize; - - /** Returns translated string for given locale and phrase */ - translate: (phrase: string, variables?: Record) => string; -}; +type LocalePickerProps = LocaleContextProps & + LocalePickerOnyxProps & { + /** Indicates size of a picker component and whether to render the label or not */ + size?: PickerSize; + }; function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal', translate}: LocalePickerProps) { + const theme = useTheme(); + const styles = useThemeStyles(); const localesToLanguages = CONST.LANGUAGES.map((language) => ({ value: language, label: translate(`languagePage.languages.${language}.label`), @@ -39,13 +40,13 @@ function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal', return; } - App.setLocale(locale); + App.setLocale(locale as ValueOf); }} items={localesToLanguages} size={size} value={preferredLocale} containerStyles={size === 'small' ? styles.pickerContainerSmall : {}} - backgroundColor={themeColors.signInPage} + backgroundColor={theme.signInPage} /> ); } @@ -53,6 +54,9 @@ function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal', LocalePicker.displayName = 'LocalePicker'; export default compose( + // TODO: think how this can be fixed + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore withLocalize, withOnyx({ preferredLocale: { diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index aba14d3b74a2..de7101b07628 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,6 +1,6 @@ import lodashDefer from 'lodash/defer'; -import React, {ForwardedRef, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {View} from 'react-native'; +import React, {ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {ScrollView, View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -177,7 +177,7 @@ function BasePicker( disableHighlight(); }), }} - scrollViewRef={context?.scrollViewRef} + scrollViewRef={context?.scrollViewRef as RefObject} scrollViewContentOffsetY={context?.contentOffsetY} /> From 548c98a6242843b82faac005714fc1cfcafee7a5 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 17 Nov 2023 13:34:16 +0100 Subject: [PATCH 08/19] Trigger snyk checks From a73ba69f65ccfb0cfac52b60e17fc98333c6c477 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 20 Nov 2023 13:20:02 +0100 Subject: [PATCH 09/19] Replace withLocalize HOC with hook usage --- src/components/LocalePicker.tsx | 33 +++++++++++++-------------------- src/components/Picker/types.ts | 2 +- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 164c1688591a..64a4b8e3f5bd 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -1,37 +1,36 @@ import React from 'react'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {LocaleContextProps} from './LocaleContextProvider'; import Picker from './Picker'; import type {PickerSize} from './Picker/types'; -import withLocalize from './withLocalize'; type LocalePickerOnyxProps = { /** Indicates which locale the user currently has selected */ preferredLocale: OnyxEntry>; }; -type LocalePickerProps = LocaleContextProps & - LocalePickerOnyxProps & { - /** Indicates size of a picker component and whether to render the label or not */ - size?: PickerSize; - }; +type LocalePickerProps = LocalePickerOnyxProps & { + /** Indicates size of a picker component and whether to render the label or not */ + size?: PickerSize; +}; -function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal', translate}: LocalePickerProps) { +function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal'}: LocalePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); + const {translate} = useLocalize(); const localesToLanguages = CONST.LANGUAGES.map((language) => ({ value: language, label: translate(`languagePage.languages.${language}.label`), keyForList: language, isSelected: preferredLocale === language, })); + return ( ({ - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - }), -)(LocalePicker); +export default withOnyx({ + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, +})(LocalePicker); diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 003b076ac922..86d1e240723f 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -49,7 +49,7 @@ type BasePickerProps = { isDisabled?: boolean; /** Input value */ - value?: string | null | number; + value?: string | number | null; /** The items to display in the list of selections */ items: PickerItem[]; From da35fc054e1bcc3e753229a6783fc32de6ad979c Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 21 Nov 2023 10:26:11 +0100 Subject: [PATCH 10/19] Update picker props type to be generic --- src/components/LocalePicker.tsx | 4 ++-- src/components/Picker/BasePicker.tsx | 10 +++++----- src/components/Picker/index.native.tsx | 4 ++-- src/components/Picker/index.tsx | 26 +++++++++++++------------- src/components/Picker/types.ts | 18 +++++++++--------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 64a4b8e3f5bd..6a12df9ef5dd 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -35,11 +35,11 @@ function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal'} { - if (locale === preferredLocale || typeof locale !== 'string') { + if (locale === preferredLocale) { return; } - App.setLocale(locale as ValueOf); + App.setLocale(locale); }} items={localesToLanguages} size={size} diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index de7101b07628..24ec13c2648c 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,5 +1,5 @@ import lodashDefer from 'lodash/defer'; -import React, {ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, ReactNode, RefObject, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {ScrollView, View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -11,7 +11,7 @@ import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; import type {BasePickerHandle, BasePickerProps} from './types'; -function BasePicker( +function BasePicker( { items, backgroundColor, @@ -35,7 +35,7 @@ function BasePicker( shouldFocusPicker = false, onBlur = () => {}, additionalPickerEvents = () => {}, - }: BasePickerProps, + }: BasePickerProps, ref: ForwardedRef, ) { const [isHighlighted, setIsHighlighted] = useState(false); @@ -68,7 +68,7 @@ function BasePicker( * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onValueChange * We are overriding this behavior to make BasePicker work with Form */ - const onValueChange = (inputValue: string, index: number) => { + const onValueChange = (inputValue: TPickerValue, index: number) => { if (inputID) { onInputChange(inputValue); return; @@ -135,7 +135,7 @@ function BasePicker( {label} )} - {value} + {value as ReactNode} {Boolean(hintText) && {hintText}} ); diff --git a/src/components/Picker/index.native.tsx b/src/components/Picker/index.native.tsx index e0b8335215b4..10ce8dcb2bba 100644 --- a/src/components/Picker/index.native.tsx +++ b/src/components/Picker/index.native.tsx @@ -2,9 +2,9 @@ import React, {ForwardedRef, forwardRef} from 'react'; import BasePicker from './BasePicker'; import {BasePickerHandle, BasePickerProps} from './types'; -function Picker(props: BasePickerProps, ref: ForwardedRef) { +function Picker(props: BasePickerProps, ref: ForwardedRef) { return ( - // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} diff --git a/src/components/Picker/index.tsx b/src/components/Picker/index.tsx index cbd09d0c0b45..5a27b54380cc 100644 --- a/src/components/Picker/index.tsx +++ b/src/components/Picker/index.tsx @@ -2,21 +2,21 @@ import React, {ForwardedRef, forwardRef} from 'react'; import BasePicker from './BasePicker'; import type {AdditionalPickerEvents, BasePickerHandle, BasePickerProps, OnChange, OnMouseDown} from './types'; -const additionalPickerEvents = (onMouseDown: OnMouseDown, onChange: OnChange): AdditionalPickerEvents => ({ - onMouseDown, - onChange: (e) => { - if (e.target.selectedIndex === undefined) { - return; - } - const index = e.target.selectedIndex; - const value = e.target.options[index].value; - onChange(value, index); - }, -}); +function Picker(props: BasePickerProps, ref: ForwardedRef) { + const additionalPickerEvents = (onMouseDown: OnMouseDown, onChange: OnChange): AdditionalPickerEvents => ({ + onMouseDown, + onChange: (e) => { + if (e.target.selectedIndex === undefined) { + return; + } + const index = e.target.selectedIndex; + const value = e.target.options[index].value; + onChange(value as TPickerValue, index); + }, + }); -function Picker(props: BasePickerProps, ref: ForwardedRef) { return ( - // eslint-disable-next-line react/jsx-props-no-spreading {...props} // Forward the ref to Picker, as we implement imperative methods there diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 86d1e240723f..98d1637ff7b3 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -12,22 +12,22 @@ type BasePickerHandle = { type OnMouseDown = () => void; -type OnChange = (value: string, index: number) => void; +type OnChange = (value: TPickerValue, index: number) => void; type AdditionalPickerEvents = { onMouseDown?: OnMouseDown; onChange?: (event: ChangeEvent) => void; }; -type AdditionalPickerEventsCallback = (onMouseDown: OnMouseDown, onChange: OnChange) => AdditionalPickerEvents; +type AdditionalPickerEventsCallback = (onMouseDown: OnMouseDown, onChange: OnChange) => AdditionalPickerEvents; type DefaultPickerEventsCallback = () => void; type PickerSize = 'normal' | 'small'; -type PickerItem = { +type PickerItem = { /** The value of the item that is being selected */ - value: string | number; + value: TPickerValue; /** The text to display for the item */ label: string; @@ -41,7 +41,7 @@ type PickerPlaceholder = { label?: string; }; -type BasePickerProps = { +type BasePickerProps = { /** BasePicker label */ label?: string | null; @@ -49,10 +49,10 @@ type BasePickerProps = { isDisabled?: boolean; /** Input value */ - value?: string | number | null; + value?: TPickerValue | null; /** The items to display in the list of selections */ - items: PickerItem[]; + items: Array>; /** Something to show as the placeholder before something is selected */ placeholder?: PickerPlaceholder; @@ -70,7 +70,7 @@ type BasePickerProps = { inputID?: string; /** A callback method that is called when the value changes and it receives the selected value as an argument */ - onInputChange: (value: string | number, index?: number) => void; + onInputChange: (value: TPickerValue, index?: number) => void; /** Size of a picker component */ size?: PickerSize; @@ -85,7 +85,7 @@ type BasePickerProps = { onBlur?: () => void; /** Additional events passed to the core BasePicker for specific platforms such as web */ - additionalPickerEvents?: AdditionalPickerEventsCallback | DefaultPickerEventsCallback; + additionalPickerEvents?: AdditionalPickerEventsCallback | DefaultPickerEventsCallback; /** Hint text that appears below the picker */ hintText?: string; From aac29daa139152ff515f98be82ce02fb8908a6fe Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 21 Nov 2023 11:24:09 +0100 Subject: [PATCH 11/19] Create separate function for default picker icon --- src/components/Picker/BasePicker.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 24ec13c2648c..cc335ff1ac5f 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,5 +1,5 @@ import lodashDefer from 'lodash/defer'; -import React, {ForwardedRef, forwardRef, ReactNode, RefObject, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {ScrollView, View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -9,7 +9,15 @@ import {ScrollContext} from '@components/ScrollViewWithContext'; import Text from '@components/Text'; import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; -import type {BasePickerHandle, BasePickerProps} from './types'; +import type {BasePickerHandle, BasePickerProps, PickerSize} from './types'; + +const getDefaultPickerIcon = (iconSize: PickerSize): ReactElement => ( + +); function BasePicker( { @@ -25,13 +33,7 @@ function BasePicker( containerStyles = [], placeholder = {}, size = 'normal', - icon = (iconSize) => ( - - ), + icon = getDefaultPickerIcon, shouldFocusPicker = false, onBlur = () => {}, additionalPickerEvents = () => {}, From a53ec873be6419793297ec4f91fad3d5ddb5ad07 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 21 Nov 2023 15:07:19 +0100 Subject: [PATCH 12/19] Create separate Locale type and add useScroll hook --- src/ONYXKEYS.ts | 2 +- src/components/LocalePicker.tsx | 4 ++-- src/components/Picker/BasePicker.tsx | 8 ++++---- src/components/ScrollViewWithContext.tsx | 1 + src/hooks/useScroll.ts | 6 ++++++ src/types/onyx/Locale.ts | 6 ++++++ src/types/onyx/index.ts | 2 ++ 7 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useScroll.ts create mode 100644 src/types/onyx/Locale.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a5a969adb833..91d054d6fad4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -379,7 +379,7 @@ type OnyxValues = { [ONYXKEYS.IS_PLAID_DISABLED]: boolean; [ONYXKEYS.PLAID_LINK_TOKEN]: string; [ONYXKEYS.ONFIDO_TOKEN]: string; - [ONYXKEYS.NVP_PREFERRED_LOCALE]: ValueOf; + [ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 6a12df9ef5dd..c04b0131744f 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -1,18 +1,18 @@ import React from 'react'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Locale} from '@src/types/onyx'; import Picker from './Picker'; import type {PickerSize} from './Picker/types'; type LocalePickerOnyxProps = { /** Indicates which locale the user currently has selected */ - preferredLocale: OnyxEntry>; + preferredLocale: OnyxEntry; }; type LocalePickerProps = LocalePickerOnyxProps & { diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index cc335ff1ac5f..1846c623ec6e 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,12 +1,12 @@ import lodashDefer from 'lodash/defer'; -import React, {ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {ScrollView, View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import {ScrollContext} from '@components/ScrollViewWithContext'; import Text from '@components/Text'; +import useScroll from '@hooks/useScroll'; import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; import type {BasePickerHandle, BasePickerProps, PickerSize} from './types'; @@ -30,7 +30,7 @@ function BasePicker( isDisabled = false, errorText = '', hintText = '', - containerStyles = [], + containerStyles, placeholder = {}, size = 'normal', icon = getDefaultPickerIcon, @@ -64,7 +64,7 @@ function BasePicker( // eslint-disable-next-line react-hooks/exhaustive-deps }, [items]); - const context = useContext(ScrollContext); + const context = useScroll(); /** * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onValueChange diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index 6122d5508a38..7c75ae2f71b2 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -63,3 +63,4 @@ ScrollViewWithContextWithRef.displayName = 'ScrollViewWithContextWithRef'; export default React.forwardRef(ScrollViewWithContextWithRef); export {ScrollContext}; +export type {ScrollContextValue}; diff --git a/src/hooks/useScroll.ts b/src/hooks/useScroll.ts new file mode 100644 index 000000000000..0b6733753893 --- /dev/null +++ b/src/hooks/useScroll.ts @@ -0,0 +1,6 @@ +import {useContext} from 'react'; +import {ScrollContext, ScrollContextValue} from '@components/ScrollViewWithContext'; + +export default function useScroll(): ScrollContextValue { + return useContext(ScrollContext); +} diff --git a/src/types/onyx/Locale.ts b/src/types/onyx/Locale.ts new file mode 100644 index 000000000000..1a5124684995 --- /dev/null +++ b/src/types/onyx/Locale.ts @@ -0,0 +1,6 @@ +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +type Locale = ValueOf; + +export default Locale; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 87cf24d6dec7..bb779b3ab58a 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -12,6 +12,7 @@ import Form, {AddDebitCardForm, DateOfBirthForm} from './Form'; import FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import Fund from './Fund'; import IOU from './IOU'; +import Locale from './Locale'; import Login from './Login'; import MapboxAccessToken from './MapboxAccessToken'; import Modal from './Modal'; @@ -67,6 +68,7 @@ export type { FrequentlyUsedEmoji, Fund, IOU, + Locale, Login, MapboxAccessToken, Modal, From 8914066ed24b727eba10f94dda2bf07bc4171fa0 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 21 Nov 2023 15:18:19 +0100 Subject: [PATCH 13/19] Rename custom hook --- src/components/Picker/BasePicker.tsx | 4 ++-- src/hooks/{useScroll.ts => useScrollContext.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/hooks/{useScroll.ts => useScrollContext.ts} (71%) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 1846c623ec6e..6d3fbfbbc410 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -6,7 +6,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; -import useScroll from '@hooks/useScroll'; +import useScrollContext from '@hooks/useScrollContext'; import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; import type {BasePickerHandle, BasePickerProps, PickerSize} from './types'; @@ -64,7 +64,7 @@ function BasePicker( // eslint-disable-next-line react-hooks/exhaustive-deps }, [items]); - const context = useScroll(); + const context = useScrollContext(); /** * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onValueChange diff --git a/src/hooks/useScroll.ts b/src/hooks/useScrollContext.ts similarity index 71% rename from src/hooks/useScroll.ts rename to src/hooks/useScrollContext.ts index 0b6733753893..711c8326bdff 100644 --- a/src/hooks/useScroll.ts +++ b/src/hooks/useScrollContext.ts @@ -1,6 +1,6 @@ import {useContext} from 'react'; import {ScrollContext, ScrollContextValue} from '@components/ScrollViewWithContext'; -export default function useScroll(): ScrollContextValue { +export default function useScrollContext(): ScrollContextValue { return useContext(ScrollContext); } From e9b526cb95e88959f4e996c297f1e5ab453dc842 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 22 Nov 2023 12:25:34 +0100 Subject: [PATCH 14/19] Minor code improvements --- src/components/Picker/BasePicker.tsx | 2 +- src/components/Picker/types.ts | 3 +++ src/components/Text.tsx | 3 --- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 6d3fbfbbc410..40ef265c999c 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -50,7 +50,7 @@ function BasePicker( // Windows will reuse the text color of the select for each one of the options // so we might need to color accordingly so it doesn't blend with the background. - const pickerPlaceholder = Object.keys(placeholder).length ? {...placeholder, color: themeColors.pickerOptionsTextColor} : {}; + const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: themeColors.pickerOptionsTextColor} : {}; useEffect(() => { if (!!value || !items || items.length !== 1 || !onInputChange) { diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 98d1637ff7b3..58eed0371893 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -69,6 +69,9 @@ type BasePickerProps = { /** The ID used to uniquely identify the input in a Form */ inputID?: string; + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft?: boolean; + /** A callback method that is called when the value changes and it receives the selected value as an argument */ onInputChange: (value: TPickerValue, index?: number) => void; diff --git a/src/components/Text.tsx b/src/components/Text.tsx index a4262e6440ad..96a6f535877a 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -21,9 +21,6 @@ type TextProps = RNTextProps & { /** The family of the font to use */ family?: keyof typeof fontFamily; - - /** Pointer events property to the Text element */ - pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto'; }; function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { From b6edfdcc48cb683841dc644f69ddd690f420f16d Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 22 Nov 2023 13:16:04 +0100 Subject: [PATCH 15/19] Update key usage --- src/components/Picker/BasePicker.tsx | 6 +++--- src/components/Picker/index.native.tsx | 1 + src/components/Picker/index.tsx | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 40ef265c999c..6ac6e11b254d 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -128,7 +128,7 @@ function BasePicker( if (isDisabled) { return ( - + {Boolean(label) && ( ( } return ( - + <> ( {Boolean(hintText) && {hintText}} - + ); } diff --git a/src/components/Picker/index.native.tsx b/src/components/Picker/index.native.tsx index 10ce8dcb2bba..7373f5a6f280 100644 --- a/src/components/Picker/index.native.tsx +++ b/src/components/Picker/index.native.tsx @@ -7,6 +7,7 @@ function Picker(props: BasePickerProps, ref: Forward // eslint-disable-next-line react/jsx-props-no-spreading {...props} + key={props.inputID} ref={ref} /> ); diff --git a/src/components/Picker/index.tsx b/src/components/Picker/index.tsx index 5a27b54380cc..18184b130bba 100644 --- a/src/components/Picker/index.tsx +++ b/src/components/Picker/index.tsx @@ -25,6 +25,7 @@ function Picker(props: BasePickerProps, ref: Forward // but doesn't open the picker (which we don't want), like it does on // Native. shouldFocusPicker + key={props.inputID} additionalPickerEvents={additionalPickerEvents} /> ); From c1df87656a43a8e7a57389b987f631e25d322caf Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 24 Nov 2023 16:53:17 +0100 Subject: [PATCH 16/19] Minor code improvements --- src/components/Picker/BasePicker.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 6ac6e11b254d..7f6a88458cc3 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -129,7 +129,7 @@ function BasePicker( if (isDisabled) { return ( - {Boolean(label) && ( + {!!label && ( ( )} {value as ReactNode} - {Boolean(hintText) && {hintText}} + {!!hintText && {hintText}} ); } @@ -184,7 +184,7 @@ function BasePicker( /> - {Boolean(hintText) && {hintText}} + {!!hintText && {hintText}} ); } From 275a908e85e944bd408dc18467c2a5ed7c1951e6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 1 Dec 2023 15:26:53 +0100 Subject: [PATCH 17/19] Updates after merging main --- package-lock.json | 14 ++++----- src/components/Picker/BasePicker.tsx | 44 ++++++++++++++++++---------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65173ccec496..25735ab77bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,7 +96,7 @@ "react-native-pdf": "^6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -44514,9 +44514,9 @@ } }, "node_modules/react-native-picker-select": { - "version": "8.0.4", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", - "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==", + "version": "8.1.0", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -84854,9 +84854,9 @@ "requires": {} }, "react-native-picker-select": { - "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", - "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==", - "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", + "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "requires": { "lodash.isequal": "^4.5.0" } diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 7f6a88458cc3..30227a19ce4f 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -1,5 +1,5 @@ import lodashDefer from 'lodash/defer'; -import React, {ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {ScrollView, View} from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -7,17 +7,11 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import useScrollContext from '@hooks/useScrollContext'; -import styles from '@styles/styles'; -import themeColors from '@styles/themes/default'; -import type {BasePickerHandle, BasePickerProps, PickerSize} from './types'; - -const getDefaultPickerIcon = (iconSize: PickerSize): ReactElement => ( - -); +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; +import type {BasePickerHandle, BasePickerProps} from './types'; + +type IconToRender = () => ReactElement; function BasePicker( { @@ -26,6 +20,7 @@ function BasePicker( inputID, value, onInputChange, + icon, label = '', isDisabled = false, errorText = '', @@ -33,13 +28,15 @@ function BasePicker( containerStyles, placeholder = {}, size = 'normal', - icon = getDefaultPickerIcon, shouldFocusPicker = false, onBlur = () => {}, additionalPickerEvents = () => {}, }: BasePickerProps, ref: ForwardedRef, ) { + const styles = useThemeStyles(); + const theme = useTheme(); + const [isHighlighted, setIsHighlighted] = useState(false); // reference to the root View @@ -50,7 +47,7 @@ function BasePicker( // Windows will reuse the text color of the select for each one of the options // so we might need to color accordingly so it doesn't blend with the background. - const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: themeColors.pickerOptionsTextColor} : {}; + const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: theme.pickerOptionsTextColor} : {}; useEffect(() => { if (!!value || !items || items.length !== 1 || !onInputChange) { @@ -87,6 +84,21 @@ function BasePicker( setIsHighlighted(false); }; + const iconToRender = useMemo((): IconToRender => { + if (icon) { + return () => icon(size); + } + + // eslint-disable-next-line react/display-name + return () => ( + + ); + }, [icon, size, styles]); + useImperativeHandle(ref, () => ({ /** * Focuses the picker (if configured to do so) @@ -153,12 +165,12 @@ function BasePicker( ({...item, color: themeColors.pickerOptionsTextColor}))} + items={items.map((item) => ({...item, color: theme.pickerOptionsTextColor}))} style={size === 'normal' ? styles.picker(isDisabled, backgroundColor) : styles.pickerSmall(backgroundColor)} useNativeAndroidPickerStyle={false} placeholder={pickerPlaceholder} value={value} - Icon={() => icon(size)} + Icon={iconToRender} disabled={isDisabled} fixAndroidTouchableBug onOpen={enableHighlight} From eb536668d2c0b5a9b7b1094556892f6978124a22 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 1 Dec 2023 15:36:20 +0100 Subject: [PATCH 18/19] Remove old BasePicker js file --- src/components/Picker/BasePicker.js | 311 ---------------------------- 1 file changed, 311 deletions(-) delete mode 100644 src/components/Picker/BasePicker.js diff --git a/src/components/Picker/BasePicker.js b/src/components/Picker/BasePicker.js deleted file mode 100644 index 084cabc02234..000000000000 --- a/src/components/Picker/BasePicker.js +++ /dev/null @@ -1,311 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import RNPickerSelect from 'react-native-picker-select'; -import _ from 'underscore'; -import FormHelpMessage from '@components/FormHelpMessage'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import refPropTypes from '@components/refPropTypes'; -import {ScrollContext} from '@components/ScrollViewWithContext'; -import Text from '@components/Text'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; - -const propTypes = { - /** A forwarded ref */ - forwardedRef: refPropTypes, - - /** BasePicker label */ - label: PropTypes.string, - - /** Should the picker appear disabled? */ - isDisabled: PropTypes.bool, - - /** Input value */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The items to display in the list of selections */ - items: PropTypes.arrayOf( - PropTypes.shape({ - /** The value of the item that is being selected */ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - - /** The text to display for the item */ - label: PropTypes.string.isRequired, - }), - ).isRequired, - - /** Something to show as the placeholder before something is selected */ - placeholder: PropTypes.shape({ - /** The value of the placeholder item, usually an empty string */ - value: PropTypes.string, - - /** The text to be displayed as the placeholder */ - label: PropTypes.string, - }), - - /** Error text to display */ - errorText: PropTypes.string, - - /** Customize the BasePicker container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the BasePicker background color */ - backgroundColor: PropTypes.string, - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - // eslint-disable-next-line react/no-unused-prop-types - shouldSaveDraft: PropTypes.bool, - - /** A callback method that is called when the value changes and it receives the selected value as an argument */ - onInputChange: PropTypes.func.isRequired, - - /** Size of a picker component */ - size: PropTypes.oneOf(['normal', 'small']), - - /** An icon to display with the picker */ - icon: PropTypes.func, - - /** Whether we should forward the focus/blur calls to the inner picker * */ - shouldFocusPicker: PropTypes.bool, - - /** Callback called when click or tap out of BasePicker */ - onBlur: PropTypes.func, - - /** Additional events passed to the core BasePicker for specific platforms such as web */ - additionalPickerEvents: PropTypes.func, - - /** Hint text that appears below the picker */ - hintText: PropTypes.string, -}; - -const defaultProps = { - forwardedRef: undefined, - label: '', - isDisabled: false, - errorText: '', - hintText: '', - containerStyles: [], - backgroundColor: undefined, - inputID: undefined, - shouldSaveDraft: false, - value: undefined, - placeholder: {}, - size: 'normal', - icon: undefined, - shouldFocusPicker: false, - onBlur: () => {}, - additionalPickerEvents: () => {}, -}; - -function BasePicker(props) { - const styles = useThemeStyles(); - const theme = useTheme(); - - const [isHighlighted, setIsHighlighted] = useState(false); - - // reference to the root View - const root = useRef(null); - - // reference to @react-native-picker/picker - const picker = useRef(null); - - // Windows will reuse the text color of the select for each one of the options - // so we might need to color accordingly so it doesn't blend with the background. - const placeholder = _.isEmpty(props.placeholder) - ? {} - : { - ...props.placeholder, - color: theme.pickerOptionsTextColor, - }; - - useEffect(() => { - if (props.value || !props.items || props.items.length !== 1 || !props.onInputChange) { - return; - } - - // When there is only 1 element in the selector, we do the user a favor and automatically select it for them - // so they don't have to spend extra time selecting the only possible value. - props.onInputChange(props.items[0].value, 0); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.items]); - - const context = useContext(ScrollContext); - - /** - * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onInputChange - * We are overriding this behavior to make BasePicker work with Form - * @param {String} value - * @param {Number} index - */ - const onInputChange = (value, index) => { - if (props.inputID) { - props.onInputChange(value); - return; - } - - props.onInputChange(value, index); - }; - - const enableHighlight = () => { - setIsHighlighted(true); - }; - - const disableHighlight = () => { - setIsHighlighted(false); - }; - - const {icon, size} = props; - - const iconToRender = useMemo(() => { - if (icon) { - return () => icon(size); - } - - // eslint-disable-next-line react/display-name - return () => ( - - ); - }, [icon, size, styles]); - - useImperativeHandle(props.forwardedRef, () => ({ - /** - * Focuses the picker (if configured to do so) - * - * This method is used by Form - */ - focus() { - if (!props.shouldFocusPicker) { - return; - } - - // Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the - // same task when we scrolled to it left that element in a glitched state, where the dropdown list can't - // be opened until the element gets re-focused - _.defer(() => { - picker.current.focus(); - }); - }, - - /** - * Like measure(), but measures the view relative to an ancestor - * - * This method is used by Form when scrolling to the input - * - * @param {Object} relativeToNativeComponentRef - reference to an ancestor - * @param {function(x: number, y: number, width: number, height: number): void} onSuccess - callback called on success - * @param {function(): void} onFail - callback called on failure - */ - measureLayout(relativeToNativeComponentRef, onSuccess, onFail) { - if (!root.current) { - return; - } - - root.current.measureLayout(relativeToNativeComponentRef, onSuccess, onFail); - }, - })); - - const hasError = !_.isEmpty(props.errorText); - - if (props.isDisabled) { - return ( - - {Boolean(props.label) && ( - - {props.label} - - )} - {props.value} - {Boolean(props.hintText) && {props.hintText}} - - ); - } - - return ( - <> - - {props.label && {props.label}} - ({...item, color: theme.pickerOptionsTextColor}))} - style={size === 'normal' ? styles.picker(props.isDisabled, props.backgroundColor) : styles.pickerSmall(props.backgroundColor)} - useNativeAndroidPickerStyle={false} - placeholder={placeholder} - value={props.value} - Icon={iconToRender} - disabled={props.isDisabled} - fixAndroidTouchableBug - onOpen={enableHighlight} - onClose={disableHighlight} - textInputProps={{ - allowFontScaling: false, - }} - pickerProps={{ - ref: picker, - tabIndex: -1, - onFocus: enableHighlight, - onBlur: () => { - disableHighlight(); - props.onBlur(); - }, - ...props.additionalPickerEvents(enableHighlight, (value, index) => { - onInputChange(value, index); - disableHighlight(); - }), - }} - scrollViewRef={context && context.scrollViewRef} - scrollViewContentOffsetY={context && context.contentOffsetY} - /> - - - {Boolean(props.hintText) && {props.hintText}} - - ); -} - -BasePicker.propTypes = propTypes; -BasePicker.defaultProps = defaultProps; -BasePicker.displayName = 'BasePicker'; - -const BasePickerWithRef = React.forwardRef((props, ref) => ( - -)); - -BasePickerWithRef.displayName = 'BasePickerWithRef'; - -export default BasePickerWithRef; From 237bb9490e6123cc7de4c6e1d1fd4a53fe10139e Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 5 Dec 2023 21:49:36 +0100 Subject: [PATCH 19/19] Update hooks call order to follow main updates --- src/components/Picker/BasePicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 30227a19ce4f..dfb2d6332da5 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -34,8 +34,8 @@ function BasePicker( }: BasePickerProps, ref: ForwardedRef, ) { - const styles = useThemeStyles(); const theme = useTheme(); + const styles = useThemeStyles(); const [isHighlighted, setIsHighlighted] = useState(false);