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

Replace OptionsSelector with SelectionList - part 1 #38994

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
daeb721
Replace OptionsSelector with SelectionList
filip-solecki Mar 26, 2024
8c372c5
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Mar 26, 2024
966d18e
Fix lint
filip-solecki Mar 26, 2024
5849045
Fix failing tests
filip-solecki Mar 26, 2024
908363b
CR fixes
filip-solecki Mar 27, 2024
46bcacc
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Mar 27, 2024
df13200
Merge fix
filip-solecki Mar 27, 2024
8c0297b
Fix isSelected state in EditReportFieldDropdownPage
filip-solecki Mar 27, 2024
50bc061
review fixes
filip-solecki Apr 2, 2024
b55a085
Remove autofocus
filip-solecki Apr 2, 2024
c8a23e4
Fix isSelected state for dropdown
filip-solecki Apr 2, 2024
1d14541
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Apr 2, 2024
d652ee0
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Apr 2, 2024
c261ba1
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Apr 3, 2024
9c11da7
CR fixes
filip-solecki Apr 3, 2024
3275787
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Apr 3, 2024
7f50fe9
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Apr 3, 2024
62528f3
Add textInputAutoFocus prop
filip-solecki Apr 4, 2024
aaeaa22
Merge branch 'main' into filip-solecki/remove-options-selector-1
filip-solecki Apr 4, 2024
f044d83
Remove autoFocus on WorkspaceSwitcher
filip-solecki Apr 4, 2024
3c3e295
revert unnecessary change
filip-solecki Apr 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/components/SelectionList/BaseListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ function BaseListItem<TItem extends ListItem>({
</View>
</View>
)}
{!item.isSelected && item.brickRoadIndicator && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

                    {!item.isSelected && !!item.brickRoadIndicator && (

<View style={[styles.alignItemsCenter, styles.justifyContentCenter]}>
<Icon
src={Expensicons.DotIndicator}
fill={item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO ? theme.iconSuccessFill : theme.danger}
/>
</View>
)}
{rightHandSideComponentRender()}
</View>
{FooterComponent}
Expand Down
26 changes: 20 additions & 6 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ function BaseSelectionList<TItem extends ListItem>(
listHeaderWrapperStyle,
isRowMultilineSupported = false,
textInputRef,
textInputIconLeft,
sectionTitleStyles,
headerMessageStyle,
shouldHideListOnInitialRender = true,
textInputAutoFocus = true,
}: BaseSelectionListProps<TItem>,
ref: ForwardedRef<SelectionListHandle>,
) {
Expand All @@ -79,7 +82,7 @@ function BaseSelectionList<TItem extends ListItem>(
const listRef = useRef<RNSectionList<TItem, SectionWithIndexOffset<TItem>>>(null);
const innerTextInputRef = useRef<RNTextInput | null>(null);
const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const shouldShowTextInput = !!textInputLabel;
const shouldShowTextInput = !!textInputLabel || !!textInputIconLeft;
const shouldShowSelectAll = !!onSelectAll;
const activeElementRole = useActiveElementRole();
const isFocused = useIsFocused();
Expand Down Expand Up @@ -310,7 +313,7 @@ function BaseSelectionList<TItem extends ListItem>(
// 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.
<View style={[styles.optionsListSectionHeader, styles.justifyContentCenter]}>
<View style={[styles.optionsListSectionHeader, styles.justifyContentCenter, sectionTitleStyles]}>
<Text style={[styles.ph4, styles.textLabelSupporting]}>{section.title}</Text>
</View>
);
Expand Down Expand Up @@ -377,6 +380,9 @@ function BaseSelectionList<TItem extends ListItem>(
/** Focuses the text input when the component comes into focus and after any navigation animations finish. */
useFocusEffect(
useCallback(() => {
if (!textInputAutoFocus) {
return;
}
if (shouldShowTextInput) {
focusTimeoutRef.current = setTimeout(() => {
if (!innerTextInputRef.current) {
Expand All @@ -391,7 +397,7 @@ function BaseSelectionList<TItem extends ListItem>(
}
clearTimeout(focusTimeoutRef.current);
};
}, [shouldShowTextInput]),
}, [shouldShowTextInput, textInputAutoFocus]),
);

const prevTextInputValue = usePrevious(textInputValue);
Expand Down Expand Up @@ -494,8 +500,12 @@ function BaseSelectionList<TItem extends ListItem>(
return;
}

// eslint-disable-next-line no-param-reassign
textInputRef.current = element as RNTextInput;
if (typeof textInputRef === 'function') {
textInputRef(element as RNTextInput);
} else {
// eslint-disable-next-line no-param-reassign
textInputRef.current = element as RNTextInput;
}
}}
label={textInputLabel}
accessibilityLabel={textInputLabel}
Expand All @@ -508,14 +518,18 @@ function BaseSelectionList<TItem extends ListItem>(
inputMode={inputMode}
selectTextOnFocus
spellCheck={false}
iconLeft={textInputIconLeft}
onSubmitEditing={selectFocusedOption}
blurOnSubmit={!!flattenedSections.allOptions.length}
isLoading={isLoadingNewOptions}
testID="selection-list-text-input"
/>
</View>
)}
{!!headerMessage && (

{/* 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 && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

                        {!isLoadingNewOptions && !!headerMessage && (

Copy link
Contributor

@Ollyws Ollyws Jun 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding !isLoadingNewOptions here caused infinite loading when it should have been displaying the user is already a member of workspace message (#42219) we fixed this in #42569

<View style={headerMessageStyle ?? [styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
Expand Down
19 changes: 16 additions & 3 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type {MutableRefObject, ReactElement, ReactNode} from 'react';
import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native';
import type {MaybePhraseKey} from '@libs/Localize';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import type CONST from '@src/CONST';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type IconAsset from '@src/types/utils/IconAsset';
import type InviteMemberListItem from './InviteMemberListItem';
import type RadioListItem from './RadioListItem';
import type TableListItem from './TableListItem';
Expand Down Expand Up @@ -110,6 +112,8 @@ type ListItem = {

/** The search value from the selection list */
searchText?: string | null;

brickRoadIndicator?: BrickRoad | '' | null;
};

type ListItemProps = CommonListItemProps<ListItem> & {
Expand Down Expand Up @@ -214,14 +218,20 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Max length for the text input */
textInputMaxLength?: number;

/** Icon to display on the left side of TextInput */
textInputIconLeft?: IconAsset;

/** Whether text input should be focused */
textInputAutoFocus?: boolean;

/** Callback to fire when the text input changes */
onChangeText?: (text: string) => void;

/** Input mode for the text input */
inputMode?: InputModeOptions;

/** Item `keyForList` to focus initially */
initiallyFocusedOptionKey?: string;
initiallyFocusedOptionKey?: string | null;

/** Callback to fire when the list is scrolled */
onScroll?: () => void;
Expand Down Expand Up @@ -272,7 +282,7 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
disableKeyboardShortcuts?: boolean;

/** Styles to apply to SelectionList container */
containerStyle?: ViewStyle;
containerStyle?: StyleProp<ViewStyle>;

/** Whether keyboard is visible on the screen */
isKeyboardShown?: boolean;
Expand All @@ -296,7 +306,10 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
isRowMultilineSupported?: boolean;

/** Ref for textInput */
textInputRef?: MutableRefObject<TextInput | null>;
textInputRef?: MutableRefObject<TextInput | null> | ((ref: TextInput | null) => void);

/** Styles for the section title */
sectionTitleStyles?: StyleProp<ViewStyle>;

/**
* When true, the list won't be visible until the list layout is measured. This prevents the list from "blinking" as it's scrolled to the bottom which is recommended for large lists.
Expand Down
21 changes: 8 additions & 13 deletions src/components/TagPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, {useMemo, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import OptionsSelector from '@components/OptionsSelector';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -100,21 +101,15 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe
const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList;

return (
<OptionsSelector
// @ts-expect-error TODO: Remove this once OptionsSelector (https://github.com/Expensify/App/issues/25125) is migrated to TypeScript.
contentContainerStyles={[{paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}]}
optionHoveredStyle={styles.hoveredComponentBG}
sectionHeaderStyle={styles.mt5}
<SelectionList
ListItem={RadioListItem}
containerStyle={{paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}}
sectionTitleStyles={styles.mt5}
sections={sections}
selectedOptions={selectedOptions}
textInputValue={searchValue}
headerMessage={headerMessage}
textInputLabel={translate('common.search')}
boldStyle
highlightSelectedOptions
textInputLabel={shouldShowTextInput ? translate('common.search') : undefined}
isRowMultilineSupported
shouldShowTextInput={shouldShowTextInput}
// Focus the first option when searching
focusedIndex={0}
// Focus the selected option on first load
initiallyFocusedOptionKey={selectedOptionKey}
onChangeText={setSearchValue}
Expand Down
6 changes: 5 additions & 1 deletion src/components/WorkspaceSwitcherButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useMemo} from 'react';
import React, {useMemo, useRef} from 'react';
import type {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
Expand All @@ -22,6 +23,7 @@ type WorkspaceSwitcherButtonProps = WorkspaceSwitcherButtonOnyxProps;
function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) {
const {translate} = useLocalize();
const theme = useTheme();
const pressableRef = useRef<View>();

const {source, name, type} = useMemo(() => {
if (!policy) {
Expand All @@ -40,10 +42,12 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) {
<Tooltip text={translate('workspace.switcher.headerTitle')}>
<PressableWithFeedback
accessibilityRole={CONST.ROLE.BUTTON}
ref={pressableRef}
accessibilityLabel={translate('common.workspaces')}
accessible
onPress={() =>
interceptAnonymousUser(() => {
pressableRef.current?.blur();
Navigation.navigate(ROUTES.WORKSPACE_SWITCHER);
})
}
Expand Down
16 changes: 9 additions & 7 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,16 +1092,18 @@ function getCategoryListSections(
*
* @param tags - an initial tag array
*/
function getTagsOptions(tags: Array<Pick<PolicyTag, 'name' | 'enabled'>>): Option[] {
function getTagsOptions(tags: Array<Pick<PolicyTag, 'name' | 'enabled'>>, selectedOptions?: SelectedTagOption[]): Option[] {
return tags.map((tag) => {
// This is to remove unnecessary escaping backslash in tag name sent from backend.
const cleanedName = PolicyUtils.getCleanedTagName(tag.name);

return {
text: cleanedName,
keyForList: tag.name,
searchText: tag.name,
tooltipText: cleanedName,
isDisabled: !tag.enabled,
isSelected: selectedOptions?.some((selectedTag) => selectedTag.name === tag.name),
};
});
}
Expand Down Expand Up @@ -1133,7 +1135,7 @@ function getTagListSections(
// "Selected" section
title: '',
shouldShow: false,
data: getTagsOptions(selectedTagOptions),
data: getTagsOptions(selectedTagOptions, selectedOptions),
});

return tagSections;
Expand All @@ -1146,7 +1148,7 @@ function getTagListSections(
// "Search" section
title: '',
shouldShow: true,
data: getTagsOptions(searchTags),
data: getTagsOptions(searchTags, selectedOptions),
});

return tagSections;
Expand All @@ -1157,7 +1159,7 @@ function getTagListSections(
// "All" section when items amount less than the threshold
title: '',
shouldShow: false,
data: getTagsOptions(enabledTags),
data: getTagsOptions(enabledTags, selectedOptions),
});

return tagSections;
Expand All @@ -1182,7 +1184,7 @@ function getTagListSections(
// "Selected" section
title: '',
shouldShow: true,
data: getTagsOptions(selectedTagOptions),
data: getTagsOptions(selectedTagOptions, selectedOptions),
});
}

Expand All @@ -1193,15 +1195,15 @@ function getTagListSections(
// "Recent" section
title: Localize.translateLocal('common.recent'),
shouldShow: true,
data: getTagsOptions(cutRecentlyUsedTags),
data: getTagsOptions(cutRecentlyUsedTags, selectedOptions),
});
}

tagSections.push({
// "All" section when items amount more than the threshold
title: Localize.translateLocal('common.all'),
shouldShow: true,
data: getTagsOptions(filteredTags),
data: getTagsOptions(filteredTags, selectedOptions),
});

return tagSections;
Expand Down
29 changes: 12 additions & 17 deletions src/pages/EditReportFieldDropdownPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React, {useMemo, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import OptionsSelector from '@components/OptionsSelector';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -42,6 +43,7 @@ type ReportFieldDropdownData = {
keyForList: string;
searchText: string;
tooltipText: string;
isSelected?: boolean;
};

type ReportFieldDropdownSectionItem = {
Expand Down Expand Up @@ -71,6 +73,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue,
keyForList: option,
searchText: option,
tooltipText: option,
isSelected: option === fieldValue,
})),
});
} else {
Expand All @@ -84,6 +87,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue,
keyForList: selectedValue,
searchText: selectedValue,
tooltipText: selectedValue,
isSelected: true,
},
],
});
Expand Down Expand Up @@ -130,27 +134,18 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue,
{({insets}) => (
<>
<HeaderWithBackButton title={fieldName} />
<OptionsSelector
// @ts-expect-error TODO: TS migration
contentContainerStyles={[{paddingBottom: getSafeAreaMargins(insets).marginBottom}]}
optionHoveredStyle={styles.hoveredComponentBG}
sectionHeaderStyle={styles.mt5}
selectedOptions={[{name: fieldValue}]}
<SelectionList
ListItem={RadioListItem}
containerStyle={{paddingBottom: getSafeAreaMargins(insets).marginBottom}}
textInputLabel={translate('common.search')}
boldStyle
sections={sections}
// Focus the first option when searching
focusedIndex={0}
value={searchValue}
onSelectRow={(option: Record<string, string>) =>
onSubmit({
[fieldKey]: fieldValue === option.text ? '' : option.text,
})
}
sectionTitleStyles={styles.mt5}
textInputValue={searchValue}
onSelectRow={(option) => onSubmit({[fieldKey]: fieldValue === option.text ? '' : option.text})}
onChangeText={setSearchValue}
highlightSelectedOptions
isRowMultilineSupported
headerMessage={headerMessage}
initiallyFocusedOptionKey={fieldValue}
/>
</>
)}
Expand Down
Loading
Loading