From e3e9e7be96ab68fa80757efe4f975f9fffd085ae Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 23 Apr 2024 09:56:45 +0200 Subject: [PATCH 01/26] wip --- .../MoneyRequestConfirmationList.tsx | 126 +++++++++++------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 99901dd261de..91ed2af2c429 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -2,10 +2,14 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {View} from 'react-native'; +import {type SectionListData, View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import TableListItem from '@components/SelectionList/TableListItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -21,6 +25,7 @@ import Log from '@libs/Log'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {PayeePersonalDetails} from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -47,6 +52,7 @@ import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import ReceiptImage from './ReceiptImage'; +import {ListItem, type Section} from './SelectionList/types'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -173,6 +179,13 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & action?: IOUAction; }; +type MemberSection = { + title: string | undefined; + shouldShow: boolean; + data: (PayeePersonalDetails | Participant | ReportUtils.OptionData)[]; + isDisabled: boolean; +}; + const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? ''; return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); @@ -219,6 +232,7 @@ function MoneyRequestConfirmationList({ lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { + console.log('ttuaj'); const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -418,7 +432,7 @@ function MoneyRequestConfirmationList({ const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; const shouldDisablePaidBySection = canModifyParticipants; const optionSelectorSections = useMemo(() => { - const sections = []; + const sections: MemberSection[] = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); @@ -448,6 +462,7 @@ function MoneyRequestConfirmationList({ title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, + isDisabled: false, }, ); } else { @@ -459,6 +474,7 @@ function MoneyRequestConfirmationList({ title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, + isDisabled: false, }); } return sections; @@ -951,53 +967,47 @@ function MoneyRequestConfirmationList({ const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - const receiptThumbnailContent = useMemo( - () => - isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ), - [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], - ); + const receiptThumbnailContent = useMemo(() => { + console.log('dupsko'); + + return isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ); + }, [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension]); + + console.log('uuu'); + console.log(isAttachmentInvalid); + console.log(isDistanceRequest); + console.log(receiptImage); + console.log(receiptThumbnail); return ( - // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - + + {isDistanceRequest && ( @@ -1036,7 +1046,27 @@ function MoneyRequestConfirmationList({ confirmText={translate('common.close')} shouldShowCancelButton={false} /> - + {footerContent} + + // + // ); } From 0ab69236201dae718a799a86402717d6e83af440 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 23 Apr 2024 12:54:38 +0200 Subject: [PATCH 02/26] wip.... --- .../MoneyRequestConfirmationList.tsx | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 91ed2af2c429..945167a82ce9 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -2,14 +2,10 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {type SectionListData, View} from 'react-native'; +import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import SelectionList from '@components/SelectionList'; -import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import TableListItem from '@components/SelectionList/TableListItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -25,7 +21,7 @@ import Log from '@libs/Log'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import {PayeePersonalDetails} from '@libs/OptionsListUtils'; +import type {PayeePersonalDetails} from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -48,11 +44,11 @@ import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import ReceiptImage from './ReceiptImage'; -import {ListItem, type Section} from './SelectionList/types'; +import SelectionList from './SelectionList'; +import InviteMemberListItem from './SelectionList/InviteMemberListItem'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -182,7 +178,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & type MemberSection = { title: string | undefined; shouldShow: boolean; - data: (PayeePersonalDetails | Participant | ReportUtils.OptionData)[]; + data: Array; isDisabled: boolean; }; @@ -232,7 +228,6 @@ function MoneyRequestConfirmationList({ lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { - console.log('ttuaj'); const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -968,8 +963,6 @@ function MoneyRequestConfirmationList({ const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); const receiptThumbnailContent = useMemo(() => { - console.log('dupsko'); - return isLocalFile && Str.isPDF(receiptFilename) ? ( + {isDistanceRequest && ( From 3f1c13c507e283497b9a60a1dd9d63eb8f65f435 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 23 Apr 2024 16:03:45 +0200 Subject: [PATCH 03/26] show tick next to payee --- .../MoneyRequestConfirmationList.tsx | 86 +++++++------------ 1 file changed, 31 insertions(+), 55 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 945167a82ce9..08aaf110a79e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -449,7 +449,7 @@ function MoneyRequestConfirmationList({ sections.push( { title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], + data: [{...formattedPayeeOption, isSelected: true}], shouldShow: true, isDisabled: shouldDisablePaidBySection, }, @@ -486,13 +486,6 @@ function MoneyRequestConfirmationList({ canModifyParticipants, ]); - const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); - useEffect(() => { if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { return; @@ -565,7 +558,7 @@ function MoneyRequestConfirmationList({ /** * Navigate to report details or profile of selected user */ - const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { + const navigateToReportOrUserDetail = (option: Participant) => { const activeRoute = Navigation.getActiveRouteWithoutParams(); if (option.isSelfDM) { @@ -962,38 +955,40 @@ function MoneyRequestConfirmationList({ const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - const receiptThumbnailContent = useMemo(() => { - return isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ); - }, [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension]); + const receiptThumbnailContent = useMemo( + () => + isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ), + [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], + ); return ( - + <> {footerContent} - - // - // + ); } From 7cd7718a44d6b6507eed65cf3cda2d2750f0f453 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 26 Apr 2024 11:42:11 +0200 Subject: [PATCH 04/26] show amount --- src/components/MoneyRequestConfirmationList.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 08aaf110a79e..2ff52c738909 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -430,7 +430,10 @@ function MoneyRequestConfirmationList({ const sections: MemberSection[] = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants).map((participant) => ({ + ...participant, + rightElement: 'descriptiveText' in participant && {participant.descriptiveText}, + })); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { @@ -449,7 +452,7 @@ function MoneyRequestConfirmationList({ sections.push( { title: translate('moneyRequestConfirmationList.paidBy'), - data: [{...formattedPayeeOption, isSelected: true}], + data: [{...formattedPayeeOption, isSelected: true, rightElement: {formattedPayeeOption.descriptiveText}}], shouldShow: true, isDisabled: shouldDisablePaidBySection, }, From b05192bb1976f6d110ffa2802035a681cd055d18 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 26 Apr 2024 12:08:06 +0200 Subject: [PATCH 05/26] align items --- src/components/MoneyRequestConfirmationList.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e3423a41ba12..7ba4c071e63a 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -444,7 +444,7 @@ function MoneyRequestConfirmationList({ title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - isDisabled: false, + isDisabled: !canModifyParticipants, }, ); } else { @@ -967,7 +967,7 @@ function MoneyRequestConfirmationList({ ); return ( - <> + )} {shouldShowAllFields && supplementaryFields} + {footerContent} - {footerContent} - + ); } From 1e65a85592c372f4633d58a088875002a3e5e895 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 26 Apr 2024 12:13:45 +0200 Subject: [PATCH 06/26] remove optionsSelector --- src/CONST.ts | 4 +- .../MoneyRequestConfirmationList.tsx | 2 +- .../OptionsSelector/BaseOptionsSelector.js | 692 ------------------ .../OptionsSelector/index.android.js | 18 - src/components/OptionsSelector/index.js | 18 - .../optionsSelectorPropTypes.js | 185 ----- .../SelectionList/BaseSelectionList.tsx | 8 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/pages/ChatFinderPage/index.tsx | 2 +- src/pages/InviteReportParticipantsPage.tsx | 2 +- src/pages/NewChatPage.tsx | 2 +- src/pages/RoomInvitePage.tsx | 2 +- src/pages/RoomMembersPage.tsx | 2 +- ...yForRefactorRequestParticipantsSelector.js | 4 +- .../ShareLogList/BaseShareLogList.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 +- .../TaskShareDestinationSelectorModal.tsx | 2 +- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- .../WorkspaceWorkflowsApproverPage.tsx | 2 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- tests/perf-test/OptionsSelector.perf-test.tsx | 141 ---- 22 files changed, 22 insertions(+), 1076 deletions(-) delete mode 100755 src/components/OptionsSelector/BaseOptionsSelector.js delete mode 100644 src/components/OptionsSelector/index.android.js delete mode 100644 src/components/OptionsSelector/index.js delete mode 100644 src/components/OptionsSelector/optionsSelectorPropTypes.js delete mode 100644 tests/perf-test/OptionsSelector.perf-test.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 2e14aa7cf21f..cac9a7904991 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3461,10 +3461,10 @@ const CONST = { BACK_BUTTON_NATIVE_ID: 'backButton', /** - * The maximum count of items per page for OptionsSelector. + * The maximum count of items per page for SelectionList. * When paginate, it multiplies by page number. */ - MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500, + MAX_SELECTION_LIST_PAGE_LENGTH: 500, /** * Bank account names diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 7ba4c071e63a..fd27275d3eec 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -144,7 +144,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** File name of the receipt */ receiptFilename?: string; - /** List styles for OptionsSelector */ + /** List styles for SelectionList */ listStyles?: StyleProp; /** Transaction that represents the expense */ diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js deleted file mode 100755 index 6515333e4015..000000000000 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ /dev/null @@ -1,692 +0,0 @@ -import lodashDebounce from 'lodash/debounce'; -import lodashFind from 'lodash/find'; -import lodashFindIndex from 'lodash/findIndex'; -import lodashGet from 'lodash/get'; -import lodashIsEqual from 'lodash/isEqual'; -import lodashMap from 'lodash/map'; -import lodashValues from 'lodash/values'; -import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import {View} from 'react-native'; -import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; -import Button from '@components/Button'; -import FixedFooter from '@components/FixedFooter'; -import FormHelpMessage from '@components/FormHelpMessage'; -import OptionsList from '@components/OptionsList'; -import ReferralProgramCTA from '@components/ReferralProgramCTA'; -import ScrollView from '@components/ScrollView'; -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 setSelection from '@libs/setSelection'; -import CONST from '@src/CONST'; -import {defaultProps as optionsSelectorDefaultProps, propTypes as optionsSelectorPropTypes} from './optionsSelectorPropTypes'; - -const propTypes = { - /** padding bottom style of safe area */ - safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** Content container styles for OptionsList */ - contentContainerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** List container styles for OptionsList */ - listContainerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** 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, - - /** Referral content type */ - referralContentType: PropTypes.string, - - ...optionsSelectorPropTypes, - ...withLocalizePropTypes, - ...withThemeStylesPropTypes, - ...withThemePropTypes, -}; - -const defaultProps = { - shouldDelayFocus: false, - shouldShowReferralCTA: false, - referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND, - safeAreaPaddingBottomStyle: {}, - contentContainerStyles: [], - listContainerStyles: undefined, - listStyles: [], - ...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 = lodashDebounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); - this.relatedTarget = null; - this.accessibilityRoles = lodashValues(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); - } - - 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(); - } - } - - 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(); - } - } - - // 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); - } - - if (prevState.paginationPage !== this.state.paginationPage) { - const newSections = this.sliceSections(); - - this.setState({ - sections: newSections, - }); - } - - if (prevState.focusedIndex !== this.state.focusedIndex) { - this.focusedOption = this.state.allOptions[this.state.focusedIndex]; - } - - if (lodashIsEqual(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 = lodashFind(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList); - const prevFocusedOptionIndex = prevFocusedOption ? lodashFindIndex(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 || (typeof this.props.focusedIndex === 'number' ? 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; - } - - // 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); - } - - this.unSubscribeFromKeyboardShortcut(); - } - - handleFocusIn() { - const activeElement = document.activeElement; - this.setState({ - disableEnterShortCut: activeElement && this.accessibilityRoles.includes(activeElement.role) && activeElement.role !== CONST.ROLE.PRESENTATION, - }); - } - - handleFocusOut() { - this.setState({ - disableEnterShortCut: false, - }); - } - - /** - * @param {Array} allOptions - * @returns {Number} - */ - getInitiallyFocusedIndex(allOptions) { - let defaultIndex; - if (this.props.shouldTextInputAppearBelowOptions) { - defaultIndex = allOptions.length; - } else if (this.props.focusedIndex >= 0) { - defaultIndex = this.props.focusedIndex; - } else { - defaultIndex = this.props.selectedOptions.length; - } - if (this.props.initiallyFocusedOptionKey === undefined) { - return defaultIndex; - } - - const indexOfInitiallyFocusedOption = lodashFindIndex(allOptions, (option) => option.keyForList === this.props.initiallyFocusedOptionKey); - - return indexOfInitiallyFocusedOption; - } - - /** - * Maps sections to render only allowed count of them per section. - * - * @returns {Objects[]} - */ - sliceSections() { - return lodashMap(this.props.sections, (section) => { - if (section.data.length === 0) { - 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. - * - * @returns {Number} - */ - calculateAllVisibleOptionsCount() { - let count = 0; - - this.state.sections.forEach((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 focusedOption = this.state.allOptions[this.state.focusedIndex]; - if (!focusedOption) { - return; - } - - this.selectRow(focusedOption); - }, - CTRLEnterConfig.descriptionKey, - CTRLEnterConfig.modifiers, - true, - ); - } - - unSubscribeFromKeyboardShortcut() { - if (this.unsubscribeEnter) { - this.unsubscribeEnter(); - } - - if (this.unsubscribeCTRLEnter) { - this.unsubscribeCTRLEnter(); - } - } - - selectFocusedOption(e) { - const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? lodashFind(this.state.allOptions, (option) => option.keyForList === focusedItemKey) : this.state.allOptions[this.state.focusedIndex]; - - if (!focusedOption || !this.props.isFocused) { - return; - } - - if (this.props.canSelectMultipleOptions) { - this.selectRow(focusedOption); - } else if (!this.state.shouldDisableRowSelection) { - this.setState({shouldDisableRowSelection: true}); - - let result = this.selectRow(focusedOption); - if (!(result instanceof Promise)) { - result = Promise.resolve(); - } - - setTimeout(() => { - result.finally(() => { - this.setState({shouldDisableRowSelection: false}); - }); - }, 500); - } - } - - // eslint-disable-next-line react/no-unused-class-component-methods - focus() { - if (!this.textInput) { - 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; - this.props.sections.forEach((section, sectionIndex) => { - section.data.forEach((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)); - } - - /** - * Scrolls to the focused index within the SectionList - * - * @param {Number} index - * @param {Boolean} animated - */ - scrollToIndex(index, animated = true) { - const option = this.state.allOptions[index]; - if (!this.list || !option) { - return; - } - - const itemIndex = option.index; - const sectionIndex = option.sectionIndex; - - if (!lodashGet(this.state.sections, `[${sectionIndex}].data[${itemIndex}]`, null)) { - return; - } - - this.list.scrollToLocation({sectionIndex, itemIndex, animated}); - } - - /** - * 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); - - if (!this.props.canSelectMultipleOptions) { - 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, - }); - }); - } - - /** - * 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); - } - } - this.props.onAddToSelection(option); - } - - /** - * 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 && this.props.selectedOptions.length === 0); - const defaultConfirmButtonText = this.props.confirmButtonText === undefined ? 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); - } - - 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 && ( - - ) - } - /> - ); - - 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} - - - )} - - {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 && ( -