From 2812c29ae8e72466f67937600a0e5bee651a5e5c Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 21 Mar 2024 04:43:10 +0530 Subject: [PATCH 01/10] fix: Fix scrollable elements in Policy pages. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 71 +++++++++++-------- src/components/SelectionList/types.ts | 2 + .../categories/WorkspaceCategoriesPage.tsx | 20 ++++-- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 015fd284c0b4..94287109386d 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -69,6 +69,7 @@ function BaseSelectionList( listHeaderWrapperStyle, isRowMultilineSupported = false, textInputRef, + ListHeaderComponent, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -339,6 +340,39 @@ function BaseSelectionList( ); }; + const header = () => ( + <> + {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( + + + + {!customListHeader && ( + e.preventDefault() : undefined} + > + {translate('workspace.people.selectAll')} + + )} + + {customListHeader} + + )} + {!headerMessage && !canSelectMultiple && customListHeader} + + ); + const scrollToFocusedIndexOnFirstRender = useCallback( (nativeEvent: LayoutChangeEvent) => { if (shouldUseDynamicMaxToRenderPerBatch) { @@ -523,34 +557,7 @@ function BaseSelectionList( ) : ( <> - {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - - - - {!customListHeader && ( - e.preventDefault() : undefined} - > - {translate('workspace.people.selectAll')} - - )} - - {customListHeader} - - )} - {!headerMessage && !canSelectMultiple && customListHeader} + {!ListHeaderComponent && header()} ( onLayout={onSectionListLayout} style={(!maxToRenderPerBatch || isInitialSectionListRender) && styles.opacity0} ListFooterComponent={ShowMoreButtonInstance} + ListHeaderComponent={ + ListHeaderComponent && ( + <> + {ListHeaderComponent} + {header()} + + ) + } /> {children} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index fac78ee786a0..504a715c0181 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -250,6 +250,8 @@ type BaseSelectionListProps = Partial & { /** Custom content to display in the header */ headerContent?: ReactNode; + ListHeaderComponent?: React.JSX.Element | null; + /** Custom content to display in the footer */ footerContent?: ReactNode; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index f3456c3875f5..55bc5ab4520c 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -261,6 +261,15 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat const shouldShowEmptyState = !categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) && !isLoading; + const getSmallWidthHeaderComponent = () => ( + <> + {getHeaderButtons()} + + {translate('workspace.categories.subtitle')} + + + ); + return ( @@ -291,10 +300,12 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat cancelText={translate('common.cancel')} danger /> - {isSmallScreenWidth && {getHeaderButtons()}} - - {translate('workspace.categories.subtitle')} - + {/* {isSmallScreenWidth && {getHeaderButtons()}} */} + {!isSmallScreenWidth && ( + + {translate('workspace.categories.subtitle')} + + )} {isLoading && ( )} From 20912d7fde785437a66b90b9cede7bae9e775290 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 2 Apr 2024 11:15:37 +0530 Subject: [PATCH 02/10] feat: sticky header component selection list. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 22 +++++++++---------- src/styles/index.ts | 4 ++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 0d8c8d06bf68..619cb2ce40ab 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -301,7 +301,7 @@ function BaseSelectionList( }; const renderSectionHeader = ({section}: {section: SectionListDataType}) => { - if (!section.title || isEmptyObject(section.data)) { + if (!section.title || isEmptyObject(section.data) || ListHeaderComponent) { return null; } @@ -345,7 +345,7 @@ function BaseSelectionList( const header = () => ( <> {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - + ( ( + <> + {renderSectionHeader(arg)} + {ListHeaderComponent && header()} + + )} renderItem={renderItem} getItemLayout={getItemLayout} onScroll={onScroll} @@ -582,14 +586,8 @@ function BaseSelectionList( onLayout={onSectionListLayout} style={(!maxToRenderPerBatch || isInitialSectionListRender) && styles.opacity0} ListFooterComponent={ShowMoreButtonInstance} - ListHeaderComponent={ - ListHeaderComponent && ( - <> - {ListHeaderComponent} - {header()} - - ) - } + ListHeaderComponent={ListHeaderComponent && ListHeaderComponent} + stickySectionHeadersEnabled={!!ListHeaderComponent && true} /> {children} diff --git a/src/styles/index.ts b/src/styles/index.ts index a736bc537fa6..6f4383fbbaff 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4338,6 +4338,10 @@ const styles = (theme: ThemeColors) => borderRadius: 8, }, + selectionListStickyHeader: { + backgroundColor: theme.dropUIBG, + }, + draggableTopBar: { height: 30, width: '100%', From 950c69d0d94657b3cf103e778a49ff1f5b8d67c7 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 2 Apr 2024 11:55:57 +0530 Subject: [PATCH 03/10] feat: make header buttons scroll on small screen width in policy pages. Signed-off-by: Krishna Gupta --- src/pages/workspace/WorkspaceMembersPage.tsx | 11 +++++++++-- .../distanceRates/PolicyDistanceRatesPage.tsx | 19 +++++++++++++++---- .../workspace/tags/WorkspaceTagsPage.tsx | 19 +++++++++++++++---- .../workspace/taxes/WorkspaceTaxesPage.tsx | 19 +++++++++++++++---- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 7b77f6b60ede..8c93fd9fe455 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -563,7 +563,6 @@ function WorkspaceMembersPage({ > {!isSmallScreenWidth && getHeaderButtons()} - {isSmallScreenWidth && {getHeaderButtons()}} toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} @@ -600,6 +599,14 @@ function WorkspaceMembersPage({ shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} textInputRef={textInputRef} customListHeader={getCustomListHeader()} + ListHeaderComponent={ + isSmallScreenWidth ? ( + + {getHeaderContent()} + {getHeaderButtons()} + + ) : null + } listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index a5356a8fd05a..6a4fdbcf6e17 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -267,6 +267,15 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) ); + const getSmallWidthHeaderComponent = () => ( + <> + {headerButtons} + + {translate('workspace.distanceRates.centrallyManage')} + + + ); + return ( @@ -287,10 +296,11 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) > {!isSmallScreenWidth && headerButtons} - {isSmallScreenWidth && {headerButtons}} - - {translate('workspace.distanceRates.centrallyManage')} - + {!isSmallScreenWidth && ( + + {translate('workspace.distanceRates.centrallyManage')} + + )} {isLoading && ( )} diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 53376c05878f..072082eda916 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -256,6 +256,15 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { ); }; + const getSmallWidthHeaderComponent = () => ( + <> + {getHeaderButtons()} + + {translate('workspace.tags.subtitle')} + + + ); + return ( @@ -287,10 +296,11 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { cancelText={translate('common.cancel')} danger /> - {isSmallScreenWidth && {getHeaderButtons()}} - - {translate('workspace.tags.subtitle')} - + {!isSmallScreenWidth && ( + + {translate('workspace.tags.subtitle')} + + )} {isLoading && ( Policy.clearPolicyTagErrors(route.params.policyID, item.value)} /> diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 4f8782dcdf3f..b46d5564aedc 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -234,6 +234,15 @@ function WorkspaceTaxesPage({ /> ); + const getSmallWidthHeaderComponent = () => ( + <> + {headerButtons} + + {translate('workspace.taxes.subtitle')} + + + ); + return ( @@ -255,11 +264,12 @@ function WorkspaceTaxesPage({ {!isSmallScreenWidth && headerButtons} - {isSmallScreenWidth && {headerButtons}} + {!isSmallScreenWidth && ( + + {translate('workspace.taxes.subtitle')} + + )} - - {translate('workspace.taxes.subtitle')} - {isLoading && ( (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} /> From d46dc8eaa13d4ba70835cb36a5ea041d7fd1b8d4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 2 Apr 2024 12:34:49 +0530 Subject: [PATCH 04/10] fix header bg color. Signed-off-by: Krishna Gupta --- src/styles/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 6f4383fbbaff..1629840a6bd0 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4339,7 +4339,7 @@ const styles = (theme: ThemeColors) => }, selectionListStickyHeader: { - backgroundColor: theme.dropUIBG, + backgroundColor: theme.appBG, }, draggableTopBar: { From 1a8bd9efc629eaa27375b7752bdfd1a4507f546d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 16 Apr 2024 19:27:49 +0530 Subject: [PATCH 05/10] revert changes in WorkspaceMembersPage & BaseSelectionList. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 292 +++++++++--------- src/pages/workspace/WorkspaceMembersPage.tsx | 136 ++++---- 2 files changed, 196 insertions(+), 232 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1d9a0a64d599..62f098e76228 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -4,7 +4,6 @@ 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 {View} from 'react-native'; -import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; import FixedFooter from '@components/FixedFooter'; @@ -16,7 +15,9 @@ import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useActiveElementRole from '@hooks/useActiveElementRole'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -56,7 +57,6 @@ function BaseSelectionList( showConfirmButton = false, shouldPreventDefaultFocusOnSelectRow = false, containerStyle, - isKeyboardShown = false, disableKeyboardShortcuts = false, children, shouldStopPropagation = false, @@ -69,7 +69,6 @@ function BaseSelectionList( listHeaderWrapperStyle, isRowMultilineSupported = false, textInputRef, - ListHeaderComponent, headerMessageStyle, shouldHideListOnInitialRender = true, textInputIconLeft, @@ -89,6 +88,7 @@ function BaseSelectionList( const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true); + const {isKeyboardShown} = useKeyboardState(); const [itemsToHighlight, setItemsToHighlight] = useState | null>(null); const itemFocusTimeoutRef = useRef(null); const [currentPage, setCurrentPage] = useState(1); @@ -167,9 +167,6 @@ function BaseSelectionList( }; }, [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 [slicedSections, ShowMoreButtonInstance] = useMemo(() => { let remainingOptionsLimit = CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage; const processedSections = getSectionsWithIndexOffset( @@ -226,6 +223,17 @@ function BaseSelectionList( [flattenedSections.allOptions], ); + // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey), + maxIndex: flattenedSections.allOptions.length - 1, + isActive: true, + onFocusedIndexChange: (index: number) => { + scrollToIndex(index, true); + }, + isFocused, + }); + /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * @@ -305,7 +313,7 @@ function BaseSelectionList( }; const renderSectionHeader = ({section}: {section: SectionListDataType}) => { - if (!section.title || isEmptyObject(section.data) || ListHeaderComponent) { + if (!section.title || isEmptyObject(section.data)) { return null; } @@ -341,43 +349,11 @@ function BaseSelectionList( rightHandSideComponent={rightHandSideComponent} keyForList={item.keyForList ?? ''} isMultilineSupported={isRowMultilineSupported} + onFocus={() => setFocusedIndex(index)} /> ); }; - const header = () => ( - <> - {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - - - - {!customListHeader && ( - e.preventDefault() : undefined} - > - {translate('workspace.people.selectAll')} - - )} - - {customListHeader} - - )} - {!headerMessage && !canSelectMultiple && customListHeader} - - ); - const scrollToFocusedIndexOnFirstRender = useCallback( (nativeEvent: LayoutChangeEvent) => { if (shouldUseDynamicMaxToRenderPerBatch) { @@ -408,7 +384,7 @@ function BaseSelectionList( setFocusedIndex(newFocusedIndex); scrollToIndex(newFocusedIndex, true); }, - [scrollToIndex], + [scrollToIndex, setFocusedIndex], ); /** Focuses the text input when the component comes into focus and after any navigation animations finish. */ @@ -494,7 +470,7 @@ function BaseSelectionList( setItemsToHighlight(null); }, timeout); }, - [flattenedSections.allOptions, updateAndScrollToFocusedIndex], + [flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex], ); useImperativeHandle(ref, () => ({scrollAndHighlightItem}), [scrollAndHighlightItem]); @@ -526,115 +502,129 @@ function BaseSelectionList( ); return ( - section.data).length - 1} - onFocusedIndexChanged={updateAndScrollToFocusedIndex} - > - - {({safeAreaPaddingBottomStyle}) => ( - - {shouldShowTextInput && ( - - { - innerTextInputRef.current = element as RNTextInput; - - if (!textInputRef) { - return; - } - - 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} - hint={textInputHint} - role={CONST.ROLE.PRESENTATION} - value={textInputValue} - placeholder={textInputPlaceholder} - maxLength={textInputMaxLength} - onChangeText={onChangeText} - inputMode={inputMode} - selectTextOnFocus - spellCheck={false} - iconLeft={textInputIconLeft} - onSubmitEditing={selectFocusedOption} - blurOnSubmit={!!flattenedSections.allOptions.length} - isLoading={isLoadingNewOptions} - testID="selection-list-text-input" - /> - - )} - {/* 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} - - )} - {!!headerContent && headerContent} - {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( - - ) : ( - <> - {!ListHeaderComponent && header()} - ( - <> - {renderSectionHeader(arg)} - {ListHeaderComponent && header()} - - )} - renderItem={renderItem} - getItemLayout={getItemLayout} - onScroll={onScroll} - onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item, index) => item.keyForList ?? `${index}`} - extraData={focusedIndex} - // the only valid values on the new arch are "white", "black", and "default", other values will cause a crash - indicatorStyle="white" - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={showScrollIndicator} - initialNumToRender={12} - maxToRenderPerBatch={maxToRenderPerBatch} - windowSize={5} - viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} - testID="selection-list" - onLayout={onSectionListLayout} - style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0} - ListFooterComponent={ShowMoreButtonInstance} - ListHeaderComponent={ListHeaderComponent && ListHeaderComponent} - stickySectionHeadersEnabled={!!ListHeaderComponent && true} - /> - {children} - - )} - {showConfirmButton && ( - -