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
27 changes: 22 additions & 5 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 ArrowKeyFocusManager from '@components/ArrowKeyFocusManager';
import Button from '@components/Button';
Expand Down Expand Up @@ -51,6 +51,7 @@ function BaseSelectionList<TItem extends ListItem>(
onConfirm,
headerContent,
footerContent,
listFooterContent,
showScrollIndicator = true,
showLoadingPlaceholder = false,
showConfirmButton = false,
Expand Down Expand Up @@ -167,7 +168,19 @@ function BaseSelectionList<TItem extends ListItem>(
}, [canSelectMultiple, sections]);

// If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey));
const [focusedIndex, setFocusedIndex] = useState(-1);
const isFocusedIndexSet = useRef(false);

useEffect(() => {
if (isFocusedIndexSet.current || (flattenedSections.allOptions.length < 1 && initiallyFocusedOptionKey === undefined)) {
return;
}
const index = flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey);
if (index !== -1) {
setFocusedIndex(index);
isFocusedIndexSet.current = true;
}
}, [flattenedSections.allOptions, initiallyFocusedOptionKey]);

const [slicedSections, ShowMoreButtonInstance] = useMemo(() => {
let remainingOptionsLimit = CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage;
Expand Down Expand Up @@ -285,7 +298,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 @@ -304,6 +317,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 @@ -320,7 +337,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 @@ -599,7 +616,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
6 changes: 3 additions & 3 deletions src/components/SelectionList/RadioListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import TextWithTooltip from '@components/TextWithTooltip';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {RadioListItemProps} from './types';
import type {ListItem, RadioListItemProps} from './types';

function RadioListItem({
function RadioListItem<TItem extends ListItem>({
item,
isFocused,
showTooltip,
Expand All @@ -16,7 +16,7 @@ function RadioListItem({
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
isMultilineSupported = false,
}: RadioListItemProps) {
}: RadioListItemProps<TItem>) {
const styles = useThemeStyles();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
const indentsLength = (item.text?.length ?? 0) - (fullTitle?.length ?? 0);
Expand Down
6 changes: 3 additions & 3 deletions src/components/SelectionList/TableListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {TableListItemProps} from './types';
import type {ListItem, TableListItemProps} from './types';

function TableListItem({
function TableListItem<TItem extends ListItem>({
item,
isFocused,
showTooltip,
Expand All @@ -23,7 +23,7 @@ function TableListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
}: TableListItemProps) {
}: TableListItemProps<TItem>) {
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();
Expand Down
6 changes: 3 additions & 3 deletions src/components/SelectionList/UserListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {UserListItemProps} from './types';
import type {ListItem, UserListItemProps} from './types';

function UserListItem({
function UserListItem<TItem extends ListItem>({
item,
isFocused,
showTooltip,
Expand All @@ -27,7 +27,7 @@ function UserListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
}: UserListItemProps) {
}: UserListItemProps<TItem>) {
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();
Expand Down
42 changes: 24 additions & 18 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import type RadioListItem from './RadioListItem';
import type TableListItem from './TableListItem';
import type UserListItem from './UserListItem';

type CommonListItemProps<TItem> = {
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> = {
/** 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 @@ -51,7 +53,7 @@ type CommonListItemProps<TItem> = {

/** Whether to wrap long text up to 2 lines */
isMultilineSupported?: boolean;
};
} & TRightHandSideComponent<TItem>;

type ListItem = {
/** Text to display */
Expand Down Expand Up @@ -116,9 +118,9 @@ type ListItem = {
brickRoadIndicator?: BrickRoad | '' | null;
};

type ListItemProps = CommonListItemProps<ListItem> & {
type ListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
/** The section list item */
item: ListItem;
item: TItem;

/** Additional styles to apply to text */
style?: StyleProp<TextStyle>;
Expand All @@ -140,10 +142,10 @@ type BaseListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
errors?: Errors | ReceiptErrors | null;
pendingAction?: PendingAction | null;
FooterComponent?: ReactElement;
children?: ReactElement<ListItemProps> | ((hovered: boolean) => ReactElement<ListItemProps>);
children?: ReactElement<ListItemProps<TItem>> | ((hovered: boolean) => ReactElement<ListItemProps<TItem>>);
};

type UserListItemProps = ListItemProps & {
type UserListItemProps<TItem extends ListItem> = ListItemProps<TItem> & {
/** Errors that this user may contain */
errors?: Errors | ReceiptErrors | null;

Expand All @@ -154,11 +156,11 @@ type UserListItemProps = ListItemProps & {
FooterComponent?: ReactElement;
};

type InviteMemberListItemProps = UserListItemProps;
type RadioListItemProps<TItem extends ListItem> = ListItemProps<TItem>;

type RadioListItemProps = ListItemProps;
type InviteMemberListItemProps<TItem extends ListItem> = UserListItemProps<TItem>;

type TableListItemProps = ListItemProps;
type TableListItemProps<TItem extends ListItem> = ListItemProps<TItem>;

type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem;

Expand All @@ -183,7 +185,7 @@ type SectionWithIndexOffset<TItem extends ListItem> = Section<TItem> & {

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 @@ -275,6 +277,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 @@ -287,9 +292,6 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Whether keyboard is visible on the screen */
isKeyboardShown?: boolean;

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

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

Expand All @@ -316,7 +318,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 @@ -337,7 +339,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
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