Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workspace switcher page list refactor #40179

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear
const yearsList = searchText === '' ? years : years.filter((year) => year.text?.includes(searchText));
return {
headerMessage: !yearsList.length ? translate('common.noResultsFound') : '',
sections: [{data: yearsList.sort((a, b) => b.value - a.value)}],
sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}],
};
}, [years, searchText, translate]);

Expand Down
2 changes: 1 addition & 1 deletion src/components/OptionsList/BaseOptionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ function BaseOptionsList(
option={item}
showTitleTooltip={showTitleTooltip}
hoverStyle={optionHoveredStyle}
optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset}
optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + (section.indexOffset ?? 0)}
onSelectRow={onSelectRow}
isSelected={isSelected}
showSelectedState={canSelectMultipleOptions}
Expand Down
2 changes: 1 addition & 1 deletion src/components/OptionsList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Section = {

type SectionWithIndexOffset = Section & {
/** The initial index of this section given the total number of options in each section's data array */
indexOffset: number;
indexOffset?: number;
};

type OptionsListProps = {
Expand Down
13 changes: 9 additions & 4 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import isEmpty from 'lodash/isEmpty';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native';
import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native';
import {View} from 'react-native';
import Button from '@components/Button';
import Checkbox from '@components/Checkbox';
Expand Down Expand Up @@ -52,6 +52,7 @@ function BaseSelectionList<TItem extends ListItem>(
onConfirm,
headerContent,
footerContent,
listFooterContent,
showScrollIndicator = true,
showLoadingPlaceholder = false,
showConfirmButton = false,
Expand Down Expand Up @@ -294,7 +295,7 @@ function BaseSelectionList<TItem extends ListItem>(
*
* [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}]
*/
const getItemLayout = (data: Array<SectionListDataType<TItem>> | null, flatDataArrayIndex: number) => {
const getItemLayout = (data: Array<SectionListData<TItem, SectionWithIndexOffset<TItem>>> | null, flatDataArrayIndex: number) => {
const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex];

if (!targetItem) {
Expand All @@ -313,6 +314,10 @@ function BaseSelectionList<TItem extends ListItem>(
};

const renderSectionHeader = ({section}: {section: SectionListDataType<TItem>}) => {
if (section.CustomSectionHeader) {
return <section.CustomSectionHeader section={section} />;
}

if (!section.title || isEmptyObject(section.data)) {
return null;
}
Expand All @@ -329,7 +334,7 @@ function BaseSelectionList<TItem extends ListItem>(
};

const renderItem = ({item, index, section}: SectionListRenderItemInfo<TItem, SectionWithIndexOffset<TItem>>) => {
const normalizedIndex = index + section.indexOffset;
const normalizedIndex = index + (section?.indexOffset ?? 0);
const isDisabled = !!section.isDisabled || item.isDisabled;
const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? ''));
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
Expand Down Expand Up @@ -603,7 +608,7 @@ function BaseSelectionList<TItem extends ListItem>(
testID="selection-list"
onLayout={onSectionListLayout}
style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0}
ListFooterComponent={ShowMoreButtonInstance}
ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance}
/>
{children}
</>
Expand Down
28 changes: 17 additions & 11 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import type RadioListItem from './RadioListItem';
import type TableListItem from './TableListItem';
import type UserListItem from './UserListItem';

type TRightHandSideComponent<TItem extends ListItem> = {
/** Component to display on the right side */
rightHandSideComponent?: ((item: TItem) => ReactElement | null | undefined) | ReactElement | null;
};

type CommonListItemProps<TItem extends ListItem> = {
/** Whether this item is focused (for arrow key controls) */
isFocused?: boolean;
Expand All @@ -34,9 +39,6 @@ type CommonListItemProps<TItem extends ListItem> = {
/** Callback to fire when an error is dismissed */
onDismissError?: (item: TItem) => void;

/** Component to display on the right side */
rightHandSideComponent?: ((item: TItem) => ReactElement<TItem> | null) | ReactElement | null;

/** Styles for the pressable component */
pressableStyle?: StyleProp<ViewStyle>;

Expand All @@ -54,7 +56,7 @@ type CommonListItemProps<TItem extends ListItem> = {

/** Handles what to do when the item is focused */
onFocus?: () => void;
};
} & TRightHandSideComponent<TItem>;

type ListItem = {
/** Text to display */
Expand Down Expand Up @@ -184,12 +186,12 @@ type Section<TItem extends ListItem> = {

type SectionWithIndexOffset<TItem extends ListItem> = Section<TItem> & {
/** The initial index of this section given the total number of options in each section's data array */
indexOffset: number;
indexOffset?: number;
};

type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Sections for the section list */
sections: Array<SectionListData<TItem, Section<TItem>>> | typeof CONST.EMPTY_ARRAY;
sections: Array<SectionListDataType<TItem>> | typeof CONST.EMPTY_ARRAY;

/** Default renderer for every item in the list */
ListItem: ValidListItem;
Expand Down Expand Up @@ -281,6 +283,9 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Custom content to display in the footer */
footerContent?: ReactNode;

/** Custom content to display in the footer of list component. If present ShowMore button won't be displayed */
listFooterContent?: React.JSX.Element | null;

/** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
shouldUseDynamicMaxToRenderPerBatch?: boolean;

Expand All @@ -293,9 +298,6 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Whether focus event should be delayed */
shouldDelayFocus?: boolean;

/** Component to display on the right side of each child */
rightHandSideComponent?: ((item: TItem) => ReactElement<TItem> | null) | ReactElement | null;

/** Whether to show the loading indicator for new options */
isLoadingNewOptions?: boolean;

Expand All @@ -322,7 +324,7 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
* When false, the list will render immediately and scroll to the bottom which works great for small lists.
*/
shouldHideListOnInitialRender?: boolean;
};
} & TRightHandSideComponent<TItem>;

type SelectionListHandle = {
scrollAndHighlightItem?: (items: string[], timeout: number) => void;
Expand All @@ -343,7 +345,11 @@ type FlattenedSectionsReturn<TItem extends ListItem> = {

type ButtonOrCheckBoxRoles = 'button' | 'checkbox';

type SectionListDataType<TItem extends ListItem> = SectionListData<TItem, SectionWithIndexOffset<TItem>>;
type ExtendedSectionListData<TItem extends ListItem, TSection extends SectionWithIndexOffset<TItem>> = SectionListData<TItem, TSection> & {
CustomSectionHeader?: ({section}: {section: TSection}) => ReactElement;
};

type SectionListDataType<TItem extends ListItem> = ExtendedSectionListData<TItem, SectionWithIndexOffset<TItem>>;

export type {
BaseSelectionListProps,
Expand Down
27 changes: 20 additions & 7 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type TaxSection = {

type CategoryTreeSection = CategorySectionBase & {
data: OptionTree[];
indexOffset?: number;
};

type Category = {
Expand Down Expand Up @@ -1023,11 +1024,13 @@ function getCategoryListSections(
const numberOfEnabledCategories = enabledCategories.length;

if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) {
const data = getCategoryOptionTree(selectedOptions, true);
categorySections.push({
// "Selected" section
title: '',
shouldShow: false,
data: getCategoryOptionTree(selectedOptions, true),
data,
indexOffset: data.length,
});

return categorySections;
Expand All @@ -1046,34 +1049,40 @@ function getCategoryListSections(
});
});

const data = getCategoryOptionTree(searchCategories, true);
categorySections.push({
// "Search" section
title: '',
shouldShow: true,
data: getCategoryOptionTree(searchCategories, true),
data,
indexOffset: data.length,
});

return categorySections;
}

if (selectedOptions.length > 0) {
const data = getCategoryOptionTree(selectedOptions, true);
categorySections.push({
// "Selected" section
title: '',
shouldShow: false,
data: getCategoryOptionTree(selectedOptions, true),
data,
indexOffset: data.length,
});
}

const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name));

if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) {
const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames);
categorySections.push({
// "All" section when items amount less than the threshold
title: '',
shouldShow: false,
data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames),
data,
indexOffset: data.length,
});

return categorySections;
Expand All @@ -1089,19 +1098,23 @@ function getCategoryListSections(
if (filteredRecentlyUsedCategories.length > 0) {
const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow);

const data = getCategoryOptionTree(cutRecentlyUsedCategories, true);
categorySections.push({
// "Recent" section
title: Localize.translateLocal('common.recent'),
shouldShow: true,
data: getCategoryOptionTree(cutRecentlyUsedCategories, true),
data,
indexOffset: data.length,
});
}

const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames);
categorySections.push({
// "All" section when items amount more than the threshold
title: Localize.translateLocal('common.all'),
shouldShow: true,
data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames),
data,
indexOffset: data.length,
});

return categorySections;
Expand Down Expand Up @@ -2356,4 +2369,4 @@ export {
getFirstKeyForList,
};

export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option};
export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree};
2 changes: 1 addition & 1 deletion src/libs/getSectionsWithIndexOffset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {SectionListData} from 'react-native';
/**
* Returns a list of sections with indexOffset
*/
export default function getSectionsWithIndexOffset<ItemT, SectionT>(sections: Array<SectionListData<ItemT, SectionT>>): Array<SectionListData<ItemT, SectionT & {indexOffset: number}>> {
export default function getSectionsWithIndexOffset<ItemT, SectionT>(sections: Array<SectionListData<ItemT, SectionT>>): Array<SectionListData<ItemT, SectionT & {indexOffset?: number}>> {
return sections.map((section, index) => {
const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + (curr.data?.length ?? 0), 0);
return {...section, indexOffset};
Expand Down
Loading
Loading