diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index d1bf02b08191..71b14b6fadcd 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -1,7 +1,7 @@ import {FlashList} from '@shopify/flash-list'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {memo, useCallback} from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -190,4 +190,4 @@ export default compose( key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, }), -)(memo(LHNOptionsList)); +)(LHNOptionsList); diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 8b24066af969..bd3695eb7aa9 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {forwardRef, memo, useEffect, useMemo, useRef} from 'react'; +import React, {forwardRef, memo, useEffect, useRef} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import OptionRow from '@components/OptionRow'; @@ -36,278 +36,268 @@ const defaultProps = { ...optionsListDefaultProps, }; -const viewabilityConfig = {viewAreaCoveragePercentThreshold: 95}; - -const BaseOptionsList = forwardRef( - ( - { - keyboardDismissMode, - onScrollBeginDrag, - onScroll, - listStyles, - focusedIndex, - selectedOptions, - headerMessage, - isLoading, - sections, - onLayout, - hideSectionHeaders, - shouldHaveOptionSeparator, - showTitleTooltip, - optionHoveredStyle, - sectionHeaderStyle, - showScrollIndicator, - contentContainerStyles: contentContainerStylesProp, - listContainerStyles: listContainerStylesProp, - shouldDisableRowInnerPadding, - shouldPreventDefaultFocusOnSelectRow, - disableFocusOptions, - canSelectMultipleOptions, - shouldShowMultipleOptionSelectorAsButton, - multipleOptionSelectorButtonText, - onAddToSelection, - highlightSelectedOptions, - onSelectRow, - boldStyle, - isDisabled, - isRowMultilineSupported, - isLoadingNewOptions, - nestedScrollEnabled, - bounces, - renderFooterContent, - safeAreaPaddingBottomStyle, - }, - innerRef, - ) => { - const styles = useThemeStyles(); - const flattenedData = useRef(); - const previousSections = usePrevious(sections); - const didLayout = useRef(false); - - const listContainerStyles = useMemo(() => listContainerStylesProp || [styles.flex1], [listContainerStylesProp, styles.flex1]); - const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, ...contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); - - /** - * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. - * - * @returns {Array} - */ - const buildFlatSectionArray = () => { - let offset = 0; - - // Start with just an empty list header - const flatArray = [{length: 0, offset}]; - - // Build the flat array - for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { - const section = sections[sectionIndex]; - - // Add the section header - const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; - flatArray.push({length: sectionHeaderHeight, offset}); - offset += sectionHeaderHeight; - - // Add section items - for (let i = 0; i < section.data.length; i++) { - let fullOptionHeight = variables.optionRowHeight; - if (i > 0 && shouldHaveOptionSeparator) { - fullOptionHeight += variables.borderTopWidth; - } - flatArray.push({length: fullOptionHeight, offset}); - offset += fullOptionHeight; +function BaseOptionsList({ + keyboardDismissMode, + onScrollBeginDrag, + onScroll, + listStyles, + focusedIndex, + selectedOptions, + headerMessage, + isLoading, + sections, + onLayout, + hideSectionHeaders, + shouldHaveOptionSeparator, + showTitleTooltip, + optionHoveredStyle, + contentContainerStyles, + sectionHeaderStyle, + showScrollIndicator, + listContainerStyles: listContainerStylesProp, + shouldDisableRowInnerPadding, + shouldPreventDefaultFocusOnSelectRow, + disableFocusOptions, + canSelectMultipleOptions, + shouldShowMultipleOptionSelectorAsButton, + multipleOptionSelectorButtonText, + onAddToSelection, + highlightSelectedOptions, + onSelectRow, + boldStyle, + isDisabled, + innerRef, + isRowMultilineSupported, + isLoadingNewOptions, + nestedScrollEnabled, + bounces, + renderFooterContent, +}) { + const styles = useThemeStyles(); + const flattenedData = useRef(); + const previousSections = usePrevious(sections); + const didLayout = useRef(false); + + const listContainerStyles = listContainerStylesProp || [styles.flex1]; + + /** + * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. + * + * @returns {Array} + */ + const buildFlatSectionArray = () => { + let offset = 0; + + // Start with just an empty list header + const flatArray = [{length: 0, offset}]; + + // Build the flat array + for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + const section = sections[sectionIndex]; + // Add the section header + const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; + flatArray.push({length: sectionHeaderHeight, offset}); + offset += sectionHeaderHeight; + + // Add section items + for (let i = 0; i < section.data.length; i++) { + let fullOptionHeight = variables.optionRowHeight; + if (i > 0 && shouldHaveOptionSeparator) { + fullOptionHeight += variables.borderTopWidth; } - - // Add the section footer - flatArray.push({length: 0, offset}); + flatArray.push({length: fullOptionHeight, offset}); + offset += fullOptionHeight; } - // Then add the list footer + // Add the section footer flatArray.push({length: 0, offset}); - return flatArray; - }; - - useEffect(() => { - if (_.isEqual(sections, previousSections)) { - return; - } + } + + // Then add the list footer + flatArray.push({length: 0, offset}); + return flatArray; + }; + + useEffect(() => { + if (_.isEqual(sections, previousSections)) { + return; + } + flattenedData.current = buildFlatSectionArray(); + }); + + const onViewableItemsChanged = () => { + if (didLayout.current || !onLayout) { + return; + } + + didLayout.current = true; + onLayout(); + }; + + /** + * This function is used to compute the layout of any given item in our list. + * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. + * + * @param {Array} data - This is the same as the data we pass into the component + * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * + * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. + * 2. Each section includes a header, even if we don't provide/render one. + * + * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: + * + * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] + * + * @returns {Object} + */ + const getItemLayout = (data, flatDataArrayIndex) => { + if (!_.has(flattenedData.current, flatDataArrayIndex)) { flattenedData.current = buildFlatSectionArray(); - }); - - const onViewableItemsChanged = () => { - if (didLayout.current || !onLayout) { - return; - } + } - didLayout.current = true; - onLayout(); + const targetItem = flattenedData.current[flatDataArrayIndex]; + return { + length: targetItem.length, + offset: targetItem.offset, + index: flatDataArrayIndex, }; - - /** - * This function is used to compute the layout of any given item in our list. - * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. - * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: - * - * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. - * 2. Each section includes a header, even if we don't provide/render one. - * - * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: - * - * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] - * - * @returns {Object} - */ - const getItemLayout = (data, flatDataArrayIndex) => { - if (!_.has(flattenedData.current, flatDataArrayIndex)) { - flattenedData.current = buildFlatSectionArray(); + }; + + /** + * Returns the key used by the list + * @param {Object} option + * @return {String} + */ + const extractKey = (option) => option.keyForList; + + /** + * Function which renders a row in the list + * + * @param {Object} params + * @param {Object} params.item + * @param {Number} params.index + * @param {Object} params.section + * + * @return {Component} + */ + const renderItem = ({item, index, section}) => { + const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; + const isSelected = _.some(selectedOptions, (option) => { + if (option.accountID && option.accountID === item.accountID) { + return true; } - const targetItem = flattenedData.current[flatDataArrayIndex]; - return { - length: targetItem.length, - offset: targetItem.offset, - index: flatDataArrayIndex, - }; - }; - - /** - * Returns the key used by the list - * - * @param {Object} option - * @return {String} - */ - const extractKey = (option) => option.keyForList; - - /** - * Function which renders a row in the list - * - * @param {Object} params - * @param {Object} params.item - * @param {Number} params.index - * @param {Object} params.section - * - * @return {Component} - */ - const renderItem = ({item, index, section}) => { - const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; - const isSelected = _.some(selectedOptions, (option) => { - if (option.accountID && option.accountID === item.accountID) { - return true; - } - - if (option.reportID && option.reportID === item.reportID) { - return true; - } - - if (_.isEmpty(option.name)) { - return false; - } - - return option.name === item.searchText; - }); - - return ( - 0 && shouldHaveOptionSeparator} - shouldDisableRowInnerPadding={shouldDisableRowInnerPadding} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} - isMultilineSupported={isRowMultilineSupported} - /> - ); - }; - - /** - * Function which renders a section header component - * - * @param {Object} params - * @param {Object} params.section - * @param {String} params.section.title - * @param {Boolean} params.section.shouldShow - * - * @return {Component} - */ - const renderSectionHeader = ({section: {title, shouldShow}}) => { - if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { - return ; + if (option.reportID && option.reportID === item.reportID) { + return true; } - if (title && shouldShow && !hideSectionHeaders) { - return ( - // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. - // We do this so that we can reference the height in `getItemLayout` – - // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. - // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. - - {title} - - ); + if (_.isEmpty(option.name)) { + return false; } - return ; - }; + return option.name === item.searchText; + }); return ( - - {isLoading ? ( - - ) : ( - <> - {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} - {/* This is misleading because we might be in the process of loading fresh options from the server. */} - {!isLoadingNewOptions && headerMessage ? ( - - {headerMessage} - - ) : null} - - - )} - + 0 && shouldHaveOptionSeparator} + shouldDisableRowInnerPadding={shouldDisableRowInnerPadding} + shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} + isMultilineSupported={isRowMultilineSupported} + /> ); - }, -); + }; + + /** + * Function which renders a section header component + * + * @param {Object} params + * @param {Object} params.section + * @param {String} params.section.title + * @param {Boolean} params.section.shouldShow + * + * @return {Component} + */ + const renderSectionHeader = ({section: {title, shouldShow}}) => { + if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { + return ; + } + + if (title && shouldShow && !hideSectionHeaders) { + return ( + // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. + // We do this so that we can reference the height in `getItemLayout` – + // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. + // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. + + {title} + + ); + } + + return ; + }; + + return ( + + {isLoading ? ( + + ) : ( + <> + {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} + {/* This is misleading because we might be in the process of loading fresh options from the server. */} + {!isLoadingNewOptions && headerMessage ? ( + + {headerMessage} + + ) : null} + + + )} + + ); +} BaseOptionsList.propTypes = propTypes; BaseOptionsList.defaultProps = defaultProps; @@ -315,7 +305,13 @@ BaseOptionsList.displayName = 'BaseOptionsList'; // using memo to avoid unnecessary rerenders when parents component rerenders (thus causing this component to rerender because shallow comparison is used for some props). export default memo( - BaseOptionsList, + forwardRef((props, ref) => ( + + )), (prevProps, nextProps) => nextProps.focusedIndex === prevProps.focusedIndex && nextProps.selectedOptions.length === prevProps.selectedOptions.length && diff --git a/src/components/OptionsList/index.js b/src/components/OptionsList/index.js index 6046a6124ccc..36b8e7fccf12 100644 --- a/src/components/OptionsList/index.js +++ b/src/components/OptionsList/index.js @@ -1,4 +1,4 @@ -import React, {forwardRef, memo, useCallback, useEffect, useRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; import {Keyboard} from 'react-native'; import _ from 'underscore'; import withWindowDimensions from '@components/withWindowDimensions'; @@ -64,4 +64,4 @@ const OptionsListWithRef = forwardRef((props, ref) => ( OptionsListWithRef.displayName = 'OptionsListWithRef'; -export default withWindowDimensions(memo(OptionsListWithRef)); +export default withWindowDimensions(OptionsListWithRef); diff --git a/src/components/OptionsList/index.native.js b/src/components/OptionsList/index.native.js index 8a70e1e060b1..ab2db4f20967 100644 --- a/src/components/OptionsList/index.native.js +++ b/src/components/OptionsList/index.native.js @@ -1,4 +1,4 @@ -import React, {forwardRef, memo} from 'react'; +import React, {forwardRef} from 'react'; import {Keyboard} from 'react-native'; import BaseOptionsList from './BaseOptionsList'; import {defaultProps, propTypes} from './optionsListPropTypes'; @@ -8,7 +8,7 @@ const OptionsList = forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onScrollBeginDrag={Keyboard.dismiss} + onScrollBeginDrag={() => Keyboard.dismiss()} /> )); @@ -16,4 +16,4 @@ OptionsList.propTypes = propTypes; OptionsList.defaultProps = defaultProps; OptionsList.displayName = 'OptionsList'; -export default memo(OptionsList); +export default OptionsList; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index b44c5375d720..792073b72613 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {InteractionManager, ScrollView, View} from 'react-native'; +import {ScrollView, View} from 'react-native'; import _ from 'underscore'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; @@ -15,7 +15,7 @@ import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigation from '@components/withNavigation'; +import withNavigationFocus from '@components/withNavigationFocus'; import withTheme, {withThemePropTypes} from '@components/withTheme'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import compose from '@libs/compose'; @@ -40,6 +40,9 @@ 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, @@ -77,19 +80,17 @@ class BaseOptionsSelector extends Component { this.incrementPage = this.incrementPage.bind(this); this.sliceSections = this.sliceSections.bind(this); this.calculateAllVisibleOptionsCount = this.calculateAllVisibleOptionsCount.bind(this); - this.onLayout = this.onLayout.bind(this); - this.setListRef = this.setListRef.bind(this); this.debouncedUpdateSearchValue = _.debounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); this.relatedTarget = null; - this.focusListener = null; - this.blurListener = null; - this.isFocused = false; + const allOptions = this.flattenSections(); + const sections = this.sliceSections(); + const focusedIndex = this.getInitiallyFocusedIndex(allOptions); this.state = { - sections: [], - allOptions: [], - focusedIndex: 0, + sections, + allOptions, + focusedIndex, shouldDisableRowSelection: false, shouldShowReferralModal: false, errorMessage: '', @@ -99,49 +100,35 @@ class BaseOptionsSelector extends Component { } componentDidMount() { - this.focusListener = this.props.navigation.addListener('focus', () => { - // 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 ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { - this.subscribeToKeyboardShortcut(); - } + this.subscribeToKeyboardShortcut(); - if (this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } + if (this.props.isFocused && this.props.autoFocus && this.textInput) { + this.focusTimeout = setTimeout(() => { + this.textInput.focus(); + }, CONST.ANIMATED_TRANSITION); + } - this.isFocused = true; - }); + this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); + } - this.blurListener = this.props.navigation.addListener('blur', () => { - if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { + componentDidUpdate(prevProps, prevState) { + if (prevProps.isFocused !== this.props.isFocused) { + if (this.props.isFocused) { + this.subscribeToKeyboardShortcut(); + } else { this.unSubscribeFromKeyboardShortcut(); } - this.isFocused = false; - }); - this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); + } - /** - * Execute the following code after all interactions have been completed. - * Which means once we are sure that all navigation animations are done, - * we will execute the callback passed to `runAfterInteractions`. - */ - this.interactionTask = InteractionManager.runAfterInteractions(() => { - const allOptions = this.flattenSections(); - const sections = this.sliceSections(); - const focusedIndex = this.getInitiallyFocusedIndex(allOptions); - this.setState({ - sections, - allOptions, - focusedIndex, - }); - }); - } + // 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 ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) { + setTimeout(() => { + this.textInput.focus(); + }, CONST.ANIMATED_TRANSITION); + } - componentDidUpdate(prevProps, prevState) { if (prevState.paginationPage !== this.state.paginationPage) { const newSections = this.sliceSections(); @@ -191,24 +178,11 @@ class BaseOptionsSelector extends Component { } componentWillUnmount() { - if (this.interactionTask) { - this.interactionTask.cancel(); - } - this.focusListener(); - this.blurListener(); if (this.focusTimeout) { clearTimeout(this.focusTimeout); } - } - - onLayout() { - if (this.props.selectedOptions.length === 0) { - this.scrollToIndex(this.state.focusedIndex, false); - } - if (this.props.onLayout) { - this.props.onLayout(); - } + this.unSubscribeFromKeyboardShortcut(); } /** @@ -237,10 +211,6 @@ class BaseOptionsSelector extends Component { return defaultIndex; } - setListRef(ref) { - this.list = ref; - } - /** * Maps sections to render only allowed count of them per section. * @@ -336,7 +306,7 @@ class BaseOptionsSelector extends Component { 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 (!focusedOption || !this.isFocused) { + if (!focusedOption || !this.props.isFocused) { return; } @@ -527,7 +497,7 @@ class BaseOptionsSelector extends Component { ); const optionsList = ( (this.list = el)} optionHoveredStyle={optionHoveredStyle} onSelectRow={this.props.onSelectRow ? this.selectRow : undefined} sections={this.state.sections} @@ -544,9 +514,16 @@ class BaseOptionsSelector extends Component { isDisabled={this.props.isDisabled} shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator} highlightSelectedOptions={this.props.highlightSelectedOptions} - onLayout={this.onLayout} - safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} - contentContainerStyles={this.props.contentContainerStyles} + 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} @@ -702,4 +679,4 @@ class BaseOptionsSelector extends Component { BaseOptionsSelector.defaultProps = defaultProps; BaseOptionsSelector.propTypes = propTypes; -export default compose(withLocalize, withNavigation, withThemeStyles, withTheme)(BaseOptionsSelector); +export default compose(withLocalize, withNavigationFocus, withThemeStyles, withTheme)(BaseOptionsSelector); diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index a6e453d9f211..061f43e73de8 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,8 +1,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -43,18 +42,6 @@ const defaultProps = { isSearchingForReports: false, }; -function isSectionsEmpty(sections) { - if (!sections.length) { - return true; - } - - if (!sections[0].data.length) { - return true; - } - - return _.isEmpty(sections[0].data[0]); -} - function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState({ @@ -67,45 +54,21 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const {translate} = useLocalize(); const themeStyles = useThemeStyles(); const isMounted = useRef(false); - const interactionTask = useRef(null); const updateOptions = useCallback(() => { - if (interactionTask.current) { - interactionTask.current.cancel(); - } - - /** - * Execute the callback after all interactions are done, which means - * after all animations have finished. - */ - interactionTask.current = InteractionManager.runAfterInteractions(() => { - const { - recentReports: localRecentReports, - personalDetails: localPersonalDetails, - userToInvite: localUserToInvite, - } = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas); - - setSearchOptions({ - recentReports: localRecentReports, - personalDetails: localPersonalDetails, - userToInvite: localUserToInvite, - }); + const { + recentReports: localRecentReports, + personalDetails: localPersonalDetails, + userToInvite: localUserToInvite, + } = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas); + + setSearchOptions({ + recentReports: localRecentReports, + personalDetails: localPersonalDetails, + userToInvite: localUserToInvite, }); }, [reports, personalDetails, searchValue, betas]); - /** - * Cancel the interaction task when the component unmounts - */ - useEffect( - () => () => { - if (!interactionTask.current) { - return; - } - interactionTask.current.cancel(); - }, - [], - ); - useEffect(() => { Timing.start(CONST.TIMING.SEARCH_RENDER); Performance.markStart(CONST.TIMING.SEARCH_RENDER); @@ -196,7 +159,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { Boolean(searchOptions.userToInvite), searchValue, ); - const sections = getSections(); + return ( _.last(props.reportActions), [props.reportActions]); - /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = useCallback(() => { + const loadOlderChats = () => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if (props.network.isOffline || props.isLoadingOlderReportActions) { return; } + const oldestReportAction = _.last(props.reportActions); + // Don't load more chats if we're already at the beginning of the chat history if (!oldestReportAction || oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, reportID]); + }; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -229,7 +229,7 @@ function ReportActionsView(props) { /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = useCallback(() => { + const recordTimeToMeasureItemLayout = () => { if (didLayout.current) { return; } @@ -244,7 +244,7 @@ function ReportActionsView(props) { } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } - }, [hasCachedActions]); + }; // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 1b7b21d2f8a8..ffcba2048d18 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; import _ from 'underscore'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; @@ -149,8 +149,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority ); const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; - // eslint-disable-next-line react-hooks/exhaustive-deps - const contentContainerStyles = useMemo(() => [styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}], [insets]); return ( @@ -189,7 +187,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority (Component) => { return WrappedComponent; }); -jest.mock('../../src/components/withNavigation', () => (Component) => { - function withNavigation(props) { +jest.mock('../../src/components/withNavigationFocus', () => (Component) => { + function WithNavigationFocus(props) { return ( jest.fn(), - }} + isFocused={false} /> ); } - withNavigation.displayName = 'withNavigation'; - return withNavigation; + WithNavigationFocus.displayName = 'WithNavigationFocus'; + + return WithNavigationFocus; }); const generateSections = (sectionConfigs) => @@ -120,10 +118,10 @@ test('[OptionsSelector] should scroll and press few items', () => { const eventData = generateEventData(100, variables.optionRowHeight); const eventData2 = generateEventData(200, variables.optionRowHeight); - const scenario = async (screen) => { + const scenario = (screen) => { fireEvent.press(screen.getByText('Item 10')); fireEvent.scroll(screen.getByTestId('options-list'), eventData); - fireEvent.press(await screen.findByText('Item 100')); + fireEvent.press(screen.getByText('Item 100')); fireEvent.scroll(screen.getByTestId('options-list'), eventData2); fireEvent.press(screen.getByText('Item 200')); };