diff --git a/package-lock.json b/package-lock.json index 1d6333ad719e..91c47de80672 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/package.json b/package.json index 1453e85fef53..366f9cf5bac4 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,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", diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 97f24a2710aa..1bc06e231448 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -398,7 +398,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.js b/src/components/LocalePicker.js deleted file mode 100644 index 83723541b16f..000000000000 --- a/src/components/LocalePicker.js +++ /dev/null @@ -1,68 +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 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 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 theme = useTheme(); - const styles = useThemeStyles(); - 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={theme.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..c04b0131744f --- /dev/null +++ b/src/components/LocalePicker.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +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; +}; + +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'}: 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 ( + { + if (locale === preferredLocale) { + return; + } + + App.setLocale(locale); + }} + items={localesToLanguages} + size={size} + value={preferredLocale} + containerStyles={size === 'small' ? styles.pickerContainerSmall : {}} + backgroundColor={theme.signInPage} + /> + ); +} + +LocalePicker.displayName = 'LocalePicker'; + +export default 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 8eaf33ab7423..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 theme = useTheme(); - const styles = useThemeStyles(); - - 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; diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx new file mode 100644 index 000000000000..dfb2d6332da5 --- /dev/null +++ b/src/components/Picker/BasePicker.tsx @@ -0,0 +1,206 @@ +import lodashDefer from 'lodash/defer'; +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'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useScrollContext from '@hooks/useScrollContext'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; +import type {BasePickerHandle, BasePickerProps} from './types'; + +type IconToRender = () => ReactElement; + +function BasePicker( + { + items, + backgroundColor, + inputID, + value, + onInputChange, + icon, + label = '', + isDisabled = false, + errorText = '', + hintText = '', + containerStyles, + placeholder = {}, + size = 'normal', + shouldFocusPicker = false, + onBlur = () => {}, + additionalPickerEvents = () => {}, + }: BasePickerProps, + ref: ForwardedRef, +) { + const theme = useTheme(); + const styles = useThemeStyles(); + + 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 > 0 ? {...placeholder, color: theme.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 = useScrollContext(); + + /** + * 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: TPickerValue, index: number) => { + if (inputID) { + onInputChange(inputValue); + return; + } + + onInputChange(inputValue, index); + }; + + const enableHighlight = () => { + setIsHighlighted(true); + }; + + const disableHighlight = () => { + 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) + * + * 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 ( + + {!!label && ( + + {label} + + )} + {value as ReactNode} + {!!hintText && {hintText}} + + ); + } + + return ( + <> + + {label && {label}} + ({...item, color: theme.pickerOptionsTextColor}))} + style={size === 'normal' ? styles.picker(isDisabled, backgroundColor) : styles.pickerSmall(backgroundColor)} + useNativeAndroidPickerStyle={false} + placeholder={pickerPlaceholder} + value={value} + Icon={iconToRender} + disabled={isDisabled} + fixAndroidTouchableBug + onOpen={enableHighlight} + onClose={disableHighlight} + textInputProps={{ + allowFontScaling: false, + }} + pickerProps={{ + ref: picker, + tabIndex: -1, + onFocus: enableHighlight, + onBlur: () => { + disableHighlight(); + onBlur(); + }, + ...additionalPickerEvents(enableHighlight, (inputValue, index) => { + onValueChange(inputValue, index); + disableHighlight(); + }), + }} + scrollViewRef={context?.scrollViewRef as RefObject} + scrollViewContentOffsetY={context?.contentOffsetY} + /> + + + {!!hintText && {hintText}} + + ); +} + +BasePicker.displayName = 'BasePicker'; + +export default forwardRef(BasePicker); diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js deleted file mode 100644 index 8e49a42e8932..000000000000 --- a/src/components/Picker/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, {forwardRef} from 'react'; -import BasePicker from './BasePicker'; - -const additionalPickerEvents = (onMouseDown, onChange) => ({ - onMouseDown, - onChange: (e) => { - if (e.target.selectedIndex === undefined) { - return; - } - const index = e.target.selectedIndex; - const value = e.target.options[index].value; - onChange(value, index); - }, -}); - -const BasePickerWithRef = forwardRef((props, ref) => ( - -)); - -BasePickerWithRef.displayName = 'BasePickerWithRef'; - -export default BasePickerWithRef; diff --git a/src/components/Picker/index.native.js b/src/components/Picker/index.native.js deleted file mode 100644 index f441609fd4d0..000000000000 --- a/src/components/Picker/index.native.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, {forwardRef} from 'react'; -import BasePicker from './BasePicker'; - -const BasePickerWithRef = forwardRef((props, ref) => ( - -)); - -BasePickerWithRef.displayName = 'BasePickerWithRef'; - -export default BasePickerWithRef; diff --git a/src/components/Picker/index.native.tsx b/src/components/Picker/index.native.tsx new file mode 100644 index 000000000000..7373f5a6f280 --- /dev/null +++ b/src/components/Picker/index.native.tsx @@ -0,0 +1,16 @@ +import React, {ForwardedRef, forwardRef} from 'react'; +import BasePicker from './BasePicker'; +import {BasePickerHandle, BasePickerProps} from './types'; + +function Picker(props: BasePickerProps, ref: ForwardedRef) { + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + key={props.inputID} + ref={ref} + /> + ); +} + +export default forwardRef(Picker); diff --git a/src/components/Picker/index.tsx b/src/components/Picker/index.tsx new file mode 100644 index 000000000000..18184b130bba --- /dev/null +++ b/src/components/Picker/index.tsx @@ -0,0 +1,34 @@ +import React, {ForwardedRef, forwardRef} from 'react'; +import BasePicker from './BasePicker'; +import type {AdditionalPickerEvents, BasePickerHandle, BasePickerProps, OnChange, OnMouseDown} from './types'; + +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); + }, + }); + + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + // Forward the ref to Picker, as we implement imperative methods there + ref={ref} + // On the Web, focusing the inner picker improves the accessibility, + // but doesn't open the picker (which we don't want), like it does on + // Native. + shouldFocusPicker + key={props.inputID} + additionalPickerEvents={additionalPickerEvents} + /> + ); +} + +export default forwardRef(Picker); diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts new file mode 100644 index 000000000000..58eed0371893 --- /dev/null +++ b/src/components/Picker/types.ts @@ -0,0 +1,97 @@ +import {ChangeEvent, Component, ReactElement} from 'react'; +import {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, 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: TPickerValue, 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: TPickerValue; + + /** 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 = { + /** BasePicker label */ + label?: string | null; + + /** Should the picker appear disabled? */ + isDisabled?: boolean; + + /** Input value */ + value?: TPickerValue | null; + + /** The items to display in the list of selections */ + items: Array>; + + /** Something to show as the placeholder before something is selected */ + placeholder?: PickerPlaceholder; + + /** Error text to display */ + errorText?: string; + + /** Customize the BasePicker container */ + containerStyles?: StyleProp; + + /** Customize the BasePicker background color */ + backgroundColor?: string; + + /** 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; + + /** 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/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/components/Text.tsx b/src/components/Text.tsx index 58a5cf300699..96a6f535877a 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -12,8 +12,10 @@ 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; diff --git a/src/hooks/useScrollContext.ts b/src/hooks/useScrollContext.ts new file mode 100644 index 000000000000..711c8326bdff --- /dev/null +++ b/src/hooks/useScrollContext.ts @@ -0,0 +1,6 @@ +import {useContext} from 'react'; +import {ScrollContext, ScrollContextValue} from '@components/ScrollViewWithContext'; + +export default function useScrollContext(): 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 4d4f45442d55..8329b56dc4b8 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'; @@ -70,6 +71,7 @@ export type { FrequentlyUsedEmoji, Fund, IOU, + Locale, Login, MapboxAccessToken, Modal,