diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index c0258f1252ef..d11e7e0a2479 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -1,9 +1,9 @@ +import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {Component} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {ScrollView, View} from 'react-native'; import _ from 'underscore'; -import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -11,13 +11,13 @@ import OptionsList from '@components/OptionsList'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import ShowMoreButton from '@components/ShowMoreButton'; import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigationFocus from '@components/withNavigationFocus'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import compose from '@libs/compose'; -import getPlatform from '@libs/getPlatform'; -import KeyboardShortcut from '@libs/KeyboardShortcut'; +import useActiveElementRole from '@hooks/useActiveElementRole'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useLocalize from '@hooks/useLocalize'; +import usePrevious from '@hooks/usePrevious'; +import useThemeStyles from '@hooks/useThemeStyles'; import setSelection from '@libs/setSelection'; import CONST from '@src/CONST'; import {defaultProps as optionsSelectorDefaultProps, propTypes as optionsSelectorPropTypes} from './optionsSelectorPropTypes'; @@ -35,9 +35,6 @@ const propTypes = { /** List styles for OptionsList */ listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - /** Whether referral CTA should be displayed */ shouldShowReferralCTA: PropTypes.bool, @@ -45,13 +42,9 @@ const propTypes = { referralContentType: PropTypes.string, ...optionsSelectorPropTypes, - ...withLocalizePropTypes, - ...withThemeStylesPropTypes, - ...withThemePropTypes, }; const defaultProps = { - shouldDelayFocus: false, shouldShowReferralCTA: false, referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND, safeAreaPaddingBottomStyle: {}, @@ -61,359 +54,187 @@ const defaultProps = { ...optionsSelectorDefaultProps, }; -class BaseOptionsSelector extends Component { - constructor(props) { - super(props); - - this.updateFocusedIndex = this.updateFocusedIndex.bind(this); - this.scrollToIndex = this.scrollToIndex.bind(this); - this.selectRow = this.selectRow.bind(this); - this.selectFocusedOption = this.selectFocusedOption.bind(this); - this.addToSelection = this.addToSelection.bind(this); - this.updateSearchValue = this.updateSearchValue.bind(this); - this.incrementPage = this.incrementPage.bind(this); - this.sliceSections = this.sliceSections.bind(this); - this.calculateAllVisibleOptionsCount = this.calculateAllVisibleOptionsCount.bind(this); - this.handleFocusIn = this.handleFocusIn.bind(this); - this.handleFocusOut = this.handleFocusOut.bind(this); - this.debouncedUpdateSearchValue = _.debounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); - this.relatedTarget = null; - this.accessibilityRoles = _.values(CONST.ROLE); - this.isWebOrDesktop = [CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()); - - const allOptions = this.flattenSections(); - const sections = this.sliceSections(); - const focusedIndex = this.getInitiallyFocusedIndex(allOptions); - this.focusedOption = allOptions[focusedIndex]; - - this.state = { - sections, - allOptions, - focusedIndex, - shouldDisableRowSelection: false, - errorMessage: '', - paginationPage: 1, - disableEnterShortCut: false, - value: '', - }; - } - - componentDidMount() { - this.subscribeToEnterShortcut(); - this.subscribeToCtrlEnterShortcut(); - this.subscribeActiveElement(); - - if (this.props.isFocused && this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } - - this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); - } +function BaseOptionsSelector(props) { + const isFocused = useIsFocused(); + const {translate} = useLocalize(); + const themeStyles = useThemeStyles(); - componentDidUpdate(prevProps, prevState) { - if (prevState.disableEnterShortCut !== this.state.disableEnterShortCut) { - // Unregister the shortcut before registering a new one to avoid lingering shortcut listener - this.unsubscribeEnter(); - if (!this.state.disableEnterShortCut) { - this.subscribeToEnterShortcut(); - } - } + const [disabledOptionsIndexes, setDisabledOptionsIndexes] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [value, setValue] = useState(''); + const [paginationPage, setPaginationPage] = useState(1); - if (prevProps.isFocused !== this.props.isFocused) { - // Unregister the shortcut before registering a new one to avoid lingering shortcut listener - this.unSubscribeFromKeyboardShortcut(); - if (this.props.isFocused) { - this.subscribeActiveElement(); - this.subscribeToEnterShortcut(); - this.subscribeToCtrlEnterShortcut(); - } else { - this.unSubscribeActiveElement(); - } - } + const shouldDisableRowSelection = useRef(false); + const relatedTarget = useRef(null); + const listRef = useRef(null); + const textInputRef = useRef(null); - // Screen coming back into focus, for example - // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. - // Only applies to platforms that support keyboard shortcuts - if (this.isWebOrDesktop && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) { - setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } + const prevSelectedOptions = usePrevious(props.selectedOptions); + const prevValue = usePrevious(value); - if (prevState.paginationPage !== this.state.paginationPage) { - const newSections = this.sliceSections(); + useImperativeHandle(props.forwardedRef, () => textInputRef.current); + const {inputCallbackRef} = useAutoFocusInput(); - this.setState({ - sections: newSections, - }); - } - - if (prevState.focusedIndex !== this.state.focusedIndex) { - this.focusedOption = this.state.allOptions[this.state.focusedIndex]; - } - - if (_.isEqual(this.props.sections, prevProps.sections)) { - return; - } - - const newSections = this.sliceSections(); - const newOptions = this.flattenSections(); - - if (prevProps.preferredLocale !== this.props.preferredLocale) { - this.setState({ - sections: newSections, - allOptions: newOptions, - }); - return; - } - const newFocusedIndex = this.props.selectedOptions.length; - const isNewFocusedIndex = newFocusedIndex !== this.state.focusedIndex; - const prevFocusedOption = _.find(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList); - const prevFocusedOptionIndex = prevFocusedOption ? _.findIndex(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList) : undefined; - // eslint-disable-next-line react/no-did-update-set-state - this.setState( - { - sections: newSections, - allOptions: newOptions, - focusedIndex: prevFocusedOptionIndex || (_.isNumber(this.props.focusedIndex) ? this.props.focusedIndex : newFocusedIndex), - }, - () => { - // If we just toggled an option on a multi-selection page or cleared the search input, scroll to top - if (this.props.selectedOptions.length !== prevProps.selectedOptions.length || (!!prevState.value && !this.state.value)) { - this.scrollToIndex(0); - return; + /** + * Paginate props.sections to only allow a certain number of items per section. + */ + const sections = useMemo( + () => + _.map(props.sections, (section) => { + if (_.isEmpty(section.data)) { + return section; } - // Otherwise, scroll to the focused index (as long as it's in range) - if (this.state.allOptions.length <= this.state.focusedIndex || !isNewFocusedIndex) { - return; - } - this.scrollToIndex(this.state.focusedIndex); - }, - ); - } - - componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout); - } + // eslint-disable-next-line no-param-reassign + section.data = section.data.slice(0, CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * (paginationPage || 1)); + return section; + }), + [paginationPage, props.sections], + ); - this.unSubscribeFromKeyboardShortcut(); - } + /** + * Flatten the sections into a single array of options. + * Each object in this array is enhanced to have: + * + * 1. A `sectionIndex`, which represents the index of the section it came from + * 2. An `index`, which represents the index of the option within the section it came from. + */ + const allOptions = useMemo(() => { + const options = []; + const calcDisabledOptionIndexes = []; + let index = 0; + _.each(sections, (section, sectionIndex) => { + _.each(section.data, (option, optionIndex) => { + // eslint-disable-next-line no-param-reassign + option.sectionIndex = sectionIndex; + // eslint-disable-next-line no-param-reassign + option.index = optionIndex; + options.push(option); - handleFocusIn() { - const activeElement = document.activeElement; - this.setState({ - disableEnterShortCut: activeElement && this.accessibilityRoles.includes(activeElement.role) && activeElement.role !== CONST.ROLE.PRESENTATION, - }); - } + if (section.isDisabled || option.isDisabled) { + calcDisabledOptionIndexes.push(index); + } - handleFocusOut() { - this.setState({ - disableEnterShortCut: false, + index++; + }); }); - } + setDisabledOptionsIndexes(calcDisabledOptionIndexes); + return options; + }, [sections]); + const prevOptions = usePrevious(allOptions); + + const initialFocusedIndex = useMemo(() => { + if (!_.isUndefined(props.initiallyFocusedOptionKey)) { + return _.findIndex(allOptions, (option) => option.keyForList === props.initiallyFocusedOptionKey); + } - /** - * @param {Array} allOptions - * @returns {Number} - */ - getInitiallyFocusedIndex(allOptions) { let defaultIndex; - if (this.props.shouldTextInputAppearBelowOptions) { + if (props.shouldTextInputAppearBelowOptions) { defaultIndex = allOptions.length; - } else if (this.props.focusedIndex >= 0) { - defaultIndex = this.props.focusedIndex; + } else if (props.focusedIndex >= 0) { + defaultIndex = props.focusedIndex; } else { - defaultIndex = this.props.selectedOptions.length; + defaultIndex = props.selectedOptions.length; } - if (_.isUndefined(this.props.initiallyFocusedOptionKey)) { - return defaultIndex; - } - - const indexOfInitiallyFocusedOption = _.findIndex(allOptions, (option) => option.keyForList === this.props.initiallyFocusedOptionKey); - - return indexOfInitiallyFocusedOption; - } + return defaultIndex; + // eslint-disable-next-line react-hooks/exhaustive-deps -- this value is only used to initialize state so only ever needs to be computed on the first render + }, []); + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex, + disabledIndexes: disabledOptionsIndexes, + maxIndex: allOptions.length - 1, + isActive: !props.disableArrowKeysActions, + disableHorizontalKeys: true, + }); /** - * Maps sections to render only allowed count of them per section. - * - * @returns {Objects[]} - */ - sliceSections() { - return _.map(this.props.sections, (section) => { - if (_.isEmpty(section.data)) { - return section; - } - - return { - ...section, - data: section.data.slice(0, CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * lodashGet(this.state, 'paginationPage', 1)), - }; - }); - } - - /** - * Calculates all currently visible options based on the sections that are currently being shown - * and the number of items of those sections. + * Completes the follow-up actions after a row is selected * - * @returns {Number} + * @param {Object} option + * @param {Object} ref + * @returns {Promise} */ - calculateAllVisibleOptionsCount() { - let count = 0; - - _.forEach(this.state.sections, (section) => { - count += lodashGet(section, 'data.length', 0); - }); - - return count; - } - - updateSearchValue(value) { - this.setState({ - paginationPage: 1, - errorMessage: value.length > this.props.maxLength ? ['common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}] : '', - value, - }); - - this.props.onChangeText(value); - } - - subscribeActiveElement() { - if (!this.isWebOrDesktop) { - return; - } - document.addEventListener('focusin', this.handleFocusIn); - document.addEventListener('focusout', this.handleFocusOut); - } - - // eslint-disable-next-line react/no-unused-class-component-methods - unSubscribeActiveElement() { - if (!this.isWebOrDesktop) { - return; - } - document.removeEventListener('focusin', this.handleFocusIn); - document.removeEventListener('focusout', this.handleFocusOut); - } - - subscribeToEnterShortcut() { - const enterConfig = CONST.KEYBOARD_SHORTCUTS.ENTER; - this.unsubscribeEnter = KeyboardShortcut.subscribe( - enterConfig.shortcutKey, - this.selectFocusedOption, - enterConfig.descriptionKey, - enterConfig.modifiers, - true, - () => !this.state.allOptions[this.state.focusedIndex], - ); - } - - subscribeToCtrlEnterShortcut() { - const CTRLEnterConfig = CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER; - this.unsubscribeCTRLEnter = KeyboardShortcut.subscribe( - CTRLEnterConfig.shortcutKey, - () => { - if (this.props.canSelectMultipleOptions) { - this.props.onConfirmSelection(); - return; + const selectRow = useCallback( + (option, ref) => + new Promise((resolve) => { + if (props.shouldShowTextInput && props.shouldPreventDefaultFocusOnSelectRow) { + if (relatedTarget.current && ref === relatedTarget.current) { + textInputRef.current.focus(); + relatedTarget.current = null; + } + if (textInputRef.current.isFocused()) { + setSelection(textInputRef.current, 0, value.length); + } } + const selectedOption = props.onSelectRow(option); + resolve(selectedOption); - const focusedOption = this.state.allOptions[this.state.focusedIndex]; - if (!focusedOption) { + if (!props.canSelectMultipleOptions) { return; } - this.selectRow(focusedOption); - }, - CTRLEnterConfig.descriptionKey, - CTRLEnterConfig.modifiers, - true, - ); - } - - unSubscribeFromKeyboardShortcut() { - if (this.unsubscribeEnter) { - this.unsubscribeEnter(); - } + // Focus the first unselected item from the list (i.e: the best result according to the current search term) + setFocusedIndex(props.selectedOptions.length); + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.shouldShowTextInput, props.shouldPreventDefaultFocusOnSelectRow, value.length, props.canSelectMultipleOptions, props.selectedOptions.length], + ); - if (this.unsubscribeCTRLEnter) { - this.unsubscribeCTRLEnter(); - } - } + const selectFocusedOption = useCallback( + (e) => { + const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value'], null); + const localFocusedOption = focusedItemKey ? _.find(allOptions, (option) => option.keyForList === focusedItemKey) : allOptions[focusedIndex]; - selectFocusedOption(e) { - const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? _.find(this.state.allOptions, (option) => option.keyForList === focusedItemKey) : this.state.allOptions[this.state.focusedIndex]; + if (!localFocusedOption || !isFocused) { + return; + } - if (!focusedOption || !this.props.isFocused) { - return; - } + if (props.canSelectMultipleOptions) { + selectRow(localFocusedOption); + } else if (!shouldDisableRowSelection.current) { + shouldDisableRowSelection.current = true; - if (this.props.canSelectMultipleOptions) { - this.selectRow(focusedOption); - } else if (!this.state.shouldDisableRowSelection) { - this.setState({shouldDisableRowSelection: true}); + let result = selectRow(localFocusedOption); + if (!(result instanceof Promise)) { + result = Promise.resolve(); + } - let result = this.selectRow(focusedOption); - if (!(result instanceof Promise)) { - result = Promise.resolve(); + setTimeout(() => { + result.finally(() => { + shouldDisableRowSelection.current = false; + }); + }, 500); } + }, + [props.canSelectMultipleOptions, focusedIndex, allOptions, isFocused, selectRow], + ); - setTimeout(() => { - result.finally(() => { - this.setState({shouldDisableRowSelection: false}); - }); - }, 500); + const selectOptions = useCallback(() => { + if (props.canSelectMultipleOptions) { + props.onConfirmSelection(); + return; } - } - // eslint-disable-next-line react/no-unused-class-component-methods - focus() { - if (!this.textInput) { + const localFocusedOption = allOptions[focusedIndex]; + if (!localFocusedOption) { return; } - this.textInput.focus(); - } - - /** - * Flattens the sections into a single array of options. - * Each object in this array is enhanced to have: - * - * 1. A `sectionIndex`, which represents the index of the section it came from - * 2. An `index`, which represents the index of the option within the section it came from. - * - * @returns {Array} - */ - flattenSections() { - const allOptions = []; - this.disabledOptionsIndexes = []; - let index = 0; - _.each(this.props.sections, (section, sectionIndex) => { - _.each(section.data, (option, optionIndex) => { - allOptions.push({ - ...option, - sectionIndex, - index: optionIndex, - }); - if (section.isDisabled || option.isDisabled) { - this.disabledOptionsIndexes.push(index); - } - index += 1; - }); - }); - return allOptions; - } - - /** - * @param {Number} index - */ - updateFocusedIndex(index) { - this.setState({focusedIndex: index}, () => this.scrollToIndex(index)); - } + selectRow(localFocusedOption); + // we don't need to include the whole props object as the dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allOptions, focusedIndex, props.canSelectMultipleOptions, props.onConfirmSelection, selectRow]); + + const activeElementRole = useActiveElementRole(); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { + shouldBubble: !allOptions[focusedIndex], + captureOnInputs: true, + isActive: isFocused && (!activeElementRole || activeElementRole === CONST.ROLE.PRESENTATION), + }); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, selectOptions, { + captureOnInputs: true, + isActive: isFocused, + }); /** * Scrolls to the focused index within the SectionList @@ -421,265 +242,273 @@ class BaseOptionsSelector extends Component { * @param {Number} index * @param {Boolean} animated */ - scrollToIndex(index, animated = true) { - const option = this.state.allOptions[index]; - if (!this.list || !option) { - return; - } + const scrollToIndex = useCallback( + (index, animated = true) => { + const option = allOptions[index]; + if (!listRef.current || !option) { + return; + } - const itemIndex = option.index; - const sectionIndex = option.sectionIndex; + const itemIndex = option.index; + const sectionIndex = option.sectionIndex; - if (!lodashGet(this.state.sections, `[${sectionIndex}].data[${itemIndex}]`, null)) { - return; - } + if (!lodashGet(sections, `[${sectionIndex}].data[${itemIndex}]`, null)) { + return; + } - this.list.scrollToLocation({sectionIndex, itemIndex, animated}); - } + listRef.current.scrollToLocation({sectionIndex, itemIndex, animated}); + }, + [allOptions, sections], + ); - /** - * Completes the follow-up actions after a row is selected - * - * @param {Object} option - * @param {Object} ref - * @returns {Promise} - */ - selectRow(option, ref) { - return new Promise((resolve) => { - if (this.props.shouldShowTextInput && this.props.shouldPreventDefaultFocusOnSelectRow) { - if (this.relatedTarget && ref === this.relatedTarget) { - this.textInput.focus(); - this.relatedTarget = null; - } - if (this.textInput.isFocused()) { - setSelection(this.textInput, 0, this.state.value.length); - } - } - const selectedOption = this.props.onSelectRow(option); - resolve(selectedOption); + useEffect(() => { + if (_.isEqual(allOptions, prevOptions)) { + return; + } - if (!this.props.canSelectMultipleOptions) { - return; - } + const newFocusedIndex = props.selectedOptions.length; + const prevFocusedOption = prevOptions[focusedIndex]; + const indexOfPrevFocusedOptionInCurrentList = _.findIndex(allOptions, (option) => prevFocusedOption && option.keyForList === prevFocusedOption.keyForList); + setFocusedIndex(indexOfPrevFocusedOptionInCurrentList || (_.isNumber(props.focusedIndex) ? props.focusedIndex : newFocusedIndex)); + // we want to run this effect only when the sections change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allOptions]); + + useEffect(() => { + // If we just toggled an option on a multi-selection page or cleared the search input, scroll to top + if (props.selectedOptions.length !== prevSelectedOptions.length || (!!prevValue && !value)) { + scrollToIndex(0); + return; + } - // Focus the first unselected item from the list (i.e: the best result according to the current search term) - this.setState({ - focusedIndex: this.props.selectedOptions.length, - }); - }); - } + // Otherwise, scroll to the focused index (as long as it's in range) + if (allOptions.length <= focusedIndex) { + return; + } + scrollToIndex(focusedIndex); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allOptions.length, focusedIndex, props.focusedIndex, props.selectedOptions, value]); + + const updateSearchValue = useCallback( + (searchValue) => { + setValue(searchValue); + setErrorMessage( + searchValue.length > props.maxLength + ? translate('common.error.characterLimitExceedCounter', { + length: searchValue.length, + limit: props.maxLength, + }) + : '', + ); + props.onChangeText(searchValue); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.onChangeText, props.maxLength, translate], + ); + + const debouncedUpdateSearchValue = _.debounce(updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); /** * Completes the follow-up action after clicking on multiple select button * @param {Object} option */ - addToSelection(option) { - if (this.props.shouldShowTextInput && this.props.shouldPreventDefaultFocusOnSelectRow) { - this.textInput.focus(); - if (this.textInput.isFocused()) { - setSelection(this.textInput, 0, this.state.value.length); + const addToSelection = useCallback( + (option) => { + if (props.shouldShowTextInput && props.shouldPreventDefaultFocusOnSelectRow) { + textInputRef.current.focus(); + if (textInputRef.current.isFocused()) { + setSelection(textInputRef.current, 0, value.length); + } } - } - this.props.onAddToSelection(option); - } + props.onAddToSelection(option); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.onAddToSelection, props.shouldShowTextInput, props.shouldPreventDefaultFocusOnSelectRow, value.length], + ); /** * Increments a pagination page to show more items */ - incrementPage() { - this.setState((prev) => ({ - paginationPage: prev.paginationPage + 1, - })); - } - - render() { - const shouldShowShowMoreButton = this.state.allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * this.state.paginationPage; - const shouldShowFooter = - !this.props.isReadOnly && (this.props.shouldShowConfirmButton || this.props.footerContent) && !(this.props.canSelectMultipleOptions && _.isEmpty(this.props.selectedOptions)); - const defaultConfirmButtonText = _.isUndefined(this.props.confirmButtonText) ? this.props.translate('common.confirm') : this.props.confirmButtonText; - const shouldShowDefaultConfirmButton = !this.props.footerContent && defaultConfirmButtonText; - const safeAreaPaddingBottomStyle = shouldShowFooter ? undefined : this.props.safeAreaPaddingBottomStyle; - const listContainerStyles = this.props.listContainerStyles || [this.props.themeStyles.flex1]; - const optionHoveredStyle = this.props.optionHoveredStyle || this.props.themeStyles.hoveredComponentBG; - - const textInput = ( - (this.textInput = el)} - label={this.props.textInputLabel} - accessibilityLabel={this.props.textInputLabel} - role={CONST.ROLE.PRESENTATION} - onChangeText={this.debouncedUpdateSearchValue} - errorText={this.state.errorMessage} - onSubmitEditing={this.selectFocusedOption} - placeholder={this.props.placeholderText} - maxLength={this.props.maxLength + CONST.ADDITIONAL_ALLOWED_CHARACTERS} - keyboardType={this.props.keyboardType} - onBlur={(e) => { - if (!this.props.shouldPreventDefaultFocusOnSelectRow) { - return; - } - this.relatedTarget = e.relatedTarget; - }} - selectTextOnFocus - blurOnSubmit={Boolean(this.state.allOptions.length)} - spellCheck={false} - shouldInterceptSwipe={this.props.shouldTextInputInterceptSwipe} - isLoading={this.props.isLoadingNewOptions} - iconLeft={this.props.textIconLeft} - testID="options-selector-input" - /> - ); - const optionsList = ( - (this.list = el)} - optionHoveredStyle={optionHoveredStyle} - onSelectRow={this.props.onSelectRow ? this.selectRow : undefined} - sections={this.state.sections} - focusedIndex={this.state.focusedIndex} - disableFocusOptions={this.props.disableFocusOptions} - selectedOptions={this.props.selectedOptions} - canSelectMultipleOptions={this.props.canSelectMultipleOptions} - shouldShowMultipleOptionSelectorAsButton={this.props.shouldShowMultipleOptionSelectorAsButton} - multipleOptionSelectorButtonText={this.props.multipleOptionSelectorButtonText} - onAddToSelection={this.addToSelection} - hideSectionHeaders={this.props.hideSectionHeaders} - headerMessage={this.state.errorMessage ? '' : this.props.headerMessage} - boldStyle={this.props.boldStyle} - showTitleTooltip={this.props.showTitleTooltip} - isDisabled={this.props.isDisabled} - shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator} - highlightSelectedOptions={this.props.highlightSelectedOptions} - onLayout={() => { - if (this.props.selectedOptions.length === 0) { - this.scrollToIndex(this.state.focusedIndex, false); - } + const incrementPage = useCallback(() => { + setPaginationPage((prev) => prev + 1); + }, []); + + const shouldShowShowMoreButton = allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * paginationPage; + const shouldShowFooter = !props.isReadOnly && (props.shouldShowConfirmButton || props.footerContent) && !(props.canSelectMultipleOptions && _.isEmpty(props.selectedOptions)); + const defaultConfirmButtonText = _.isUndefined(props.confirmButtonText) ? translate('common.confirm') : props.confirmButtonText; + const shouldShowDefaultConfirmButton = !props.footerContent && defaultConfirmButtonText; + const safeAreaPaddingBottomStyle = shouldShowFooter ? undefined : props.safeAreaPaddingBottomStyle; + const listContainerStyles = props.listContainerStyles || [themeStyles.flex1]; + const optionHoveredStyle = props.optionHoveredStyle || themeStyles.hoveredComponentBG; + + const textInput = ( + { + textInputRef.current = el; + inputCallbackRef(el); + }} + label={props.textInputLabel} + accessibilityLabel={props.textInputLabel} + role={CONST.ROLE.PRESENTATION} + onChangeText={debouncedUpdateSearchValue} + errorText={errorMessage} + onSubmitEditing={selectFocusedOption} + placeholder={props.placeholderText} + maxLength={props.maxLength + CONST.ADDITIONAL_ALLOWED_CHARACTERS} + keyboardType={props.keyboardType} + onBlur={(e) => { + if (!props.shouldPreventDefaultFocusOnSelectRow) { + return; + } + relatedTarget.current = e.relatedTarget; + }} + selectTextOnFocus + blurOnSubmit={Boolean(allOptions.length)} + spellCheck={false} + shouldInterceptSwipe={props.shouldTextInputInterceptSwipe} + isLoading={props.isLoadingNewOptions} + iconLeft={props.textIconLeft} + testID="options-selector-input" + /> + ); + const optionsList = ( + { + if (props.selectedOptions.length === 0) { + scrollToIndex(focusedIndex, false); + } - if (this.props.onLayout) { - this.props.onLayout(); - } - }} - contentContainerStyles={[safeAreaPaddingBottomStyle, ...this.props.contentContainerStyles]} - sectionHeaderStyle={this.props.sectionHeaderStyle} - listContainerStyles={listContainerStyles} - listStyles={this.props.listStyles} - isLoading={!this.props.shouldShowOptions} - showScrollIndicator={this.props.showScrollIndicator} - isRowMultilineSupported={this.props.isRowMultilineSupported} - isLoadingNewOptions={this.props.isLoadingNewOptions} - shouldPreventDefaultFocusOnSelectRow={this.props.shouldPreventDefaultFocusOnSelectRow} - nestedScrollEnabled={this.props.nestedScrollEnabled} - bounces={!this.props.shouldTextInputAppearBelowOptions || !this.props.shouldAllowScrollingChildren} - renderFooterContent={ - shouldShowShowMoreButton && ( - - ) + if (props.onLayout) { + props.onLayout(); } - /> - ); - - const optionsAndInputsBelowThem = ( - <> - - {optionsList} - - - {this.props.children} - {this.props.shouldShowTextInput && textInput} - - - ); - - return ( - {} : this.updateFocusedIndex} - shouldResetIndexOnEndReached={false} - > - - {/* - * The OptionsList component uses a SectionList which uses a VirtualizedList internally. - * VirtualizedList cannot be directly nested within ScrollViews of the same orientation. - * To work around this, we wrap the OptionsList component with a horizontal ScrollView. - */} - {this.props.shouldTextInputAppearBelowOptions && this.props.shouldAllowScrollingChildren && ( - - - {optionsAndInputsBelowThem} - + }} + contentContainerStyles={[safeAreaPaddingBottomStyle, ...props.contentContainerStyles]} + sectionHeaderStyle={props.sectionHeaderStyle} + listContainerStyles={listContainerStyles} + listStyles={props.listStyles} + isLoading={!props.shouldShowOptions} + showScrollIndicator={props.showScrollIndicator} + isRowMultilineSupported={props.isRowMultilineSupported} + isLoadingNewOptions={props.isLoadingNewOptions} + shouldPreventDefaultFocusOnSelectRow={props.shouldPreventDefaultFocusOnSelectRow} + nestedScrollEnabled={props.nestedScrollEnabled} + bounces={!props.shouldTextInputAppearBelowOptions || !props.shouldAllowScrollingChildren} + renderFooterContent={() => + shouldShowShowMoreButton && ( + + ) + } + /> + ); + + const optionsAndInputsBelowThem = ( + <> + {optionsList} + + {props.children} + {props.shouldShowTextInput && textInput} + + + ); + + return ( + <> + + {/* + * The OptionsList component uses a SectionList which uses a VirtualizedList internally. + * VirtualizedList cannot be directly nested within ScrollViews of the same orientation. + * To work around this, we wrap the OptionsList component with a horizontal ScrollView. + */} + {props.shouldTextInputAppearBelowOptions && props.shouldAllowScrollingChildren && ( + + + {optionsAndInputsBelowThem} - )} - - {this.props.shouldTextInputAppearBelowOptions && !this.props.shouldAllowScrollingChildren && optionsAndInputsBelowThem} - - {!this.props.shouldTextInputAppearBelowOptions && ( - <> - - {this.props.children} - {this.props.shouldShowTextInput && textInput} - {Boolean(this.props.textInputAlert) && ( - - )} - - {optionsList} - - )} - - {this.props.shouldShowReferralCTA && ( - - - + )} - {shouldShowFooter && ( - - {shouldShowDefaultConfirmButton && ( -