Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type FilterListProps = FilterItemCallbacks & {
/** Content at the top of the list, scrolled with its rows. Pass a falsy value when the search hides it, so the list knows nothing is listed. */
headerContent?: ReactNode;

/** Content below the rows, scrolled with them. */
children?: ReactNode;

/** Called as the cursor travels over the list. Web only - it reports the movement inside a row, which `onHoverIn` only reports entering. */
onPointerMove?: (event: {clientX: number; clientY: number}) => void;

Expand Down Expand Up @@ -135,6 +138,7 @@ function FilterList({
onSearchValueChange,
searchInputRef,
headerContent,
children,
onHoverIn,
onFocus,
onPress,
Expand Down Expand Up @@ -196,6 +200,7 @@ function FilterList({
))}
</View>
))}
{children}
</ScrollView>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useShouldFooterBeInsideList from '@hooks/useShouldFooterBeInsideList';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -38,6 +39,7 @@ function SearchAdvancedFiltersBase() {
const {currentDraftFilters, shouldShowResetFilters} = useContext(SearchAdvancedFiltersContext);
const {applyFilters, resetFilters} = useContext(SearchAdvancedFiltersActionContext);
const isInLandscapeMode = useIsInLandscapeMode();
const shouldFooterBeInsideList = useShouldFooterBeInsideList();
const StyleUtils = useStyleUtils();
const {isBetaEnabled} = usePermissions();
const canUseNLFilters = isBetaEnabled(CONST.BETAS.NL_FILTERS);
Expand All @@ -49,6 +51,28 @@ function SearchAdvancedFiltersBase() {
const filters = getMatchingFilters(allFilters, currentDraftFilters.type, listedSearchValue, translate);
const isDescribeSearchListed = canUseNLFilters && isLabelMatchingSearchValue(translate('search.filters.describeSearch.title'), listedSearchValue);

const footer = (
<View style={[isInLandscapeMode ? [styles.flexRow, styles.gap2] : [styles.gap3], styles.ph5, styles.pb5]}>
{shouldShowResetFilters && (
<Button
style={[isInLandscapeMode ? styles.flex1 : undefined]}
size={CONST.BUTTON_SIZE.LARGE}
onPress={resetFilters}
>
<Button.Text>{translate('common.reset')}</Button.Text>
</Button>
)}
<Button
style={[isInLandscapeMode ? styles.flex1 : undefined]}
variant={CONST.BUTTON_VARIANT.SUCCESS}
size={CONST.BUTTON_SIZE.LARGE}
onPress={applyFilters}
>
<Button.Text>{translate('search.applyFilters')}</Button.Text>
</Button>
</View>
);

return (
<ScreenWrapper
testID="SearchAdvancedFiltersPage"
Expand Down Expand Up @@ -99,26 +123,10 @@ function SearchAdvancedFiltersBase() {
)
}
onPress={(filterKey) => Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS_CONTENT.getRoute(filterKey))}
/>
<View style={[isInLandscapeMode ? [styles.flexRow, styles.gap2] : [styles.gap3], styles.ph5, styles.pb5]}>
{shouldShowResetFilters && (
<Button
style={[isInLandscapeMode ? styles.flex1 : undefined]}
size={CONST.BUTTON_SIZE.LARGE}
onPress={resetFilters}
>
<Button.Text>{translate('common.reset')}</Button.Text>
</Button>
)}
<Button
style={[isInLandscapeMode ? styles.flex1 : undefined]}
variant={CONST.BUTTON_VARIANT.SUCCESS}
size={CONST.BUTTON_SIZE.LARGE}
onPress={applyFilters}
>
<Button.Text>{translate('search.applyFilters')}</Button.Text>
</Button>
</View>
>
{shouldFooterBeInsideList && footer}
</FilterList>
{!shouldFooterBeInsideList && footer}
</ScreenWrapper>
);
}
Expand Down
76 changes: 76 additions & 0 deletions tests/ui/SearchAdvancedFiltersBaseTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {render, screen, within} from '@testing-library/react-native';

import useShouldFooterBeInsideList from '@hooks/useShouldFooterBeInsideList';

import IntlStore from '@src/languages/IntlStore';
import SearchAdvancedFiltersBase from '@src/pages/Search/SearchAdvancedFiltersPage/SearchAdvancedFiltersBase';

import type {ReactNode} from 'react';

import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import {View} from 'react-native';

import type * as TestHelper from '../utils/TestHelper';

// jest.mock factories can't reference imported bindings, but `mock`-prefixed locals are allowed.
const MockView = View;

// Stand-in for the filter list that only renders what the page puts inside its scroll area.
jest.mock('@components/Search/FilterComponents/AdvancedFilters/FilterList', () => ({
__esModule: true,
default: ({children}: {children?: ReactNode}) => <MockView testID="filter-list">{children}</MockView>,
}));

jest.mock('@hooks/useShouldFooterBeInsideList', () => jest.fn(() => false));

jest.mock('@hooks/useAdvancedSearchFilters', () => jest.fn(() => []));

jest.mock('@hooks/usePermissions', () => ({
__esModule: true,
default: () => ({isBetaEnabled: () => false}),
}));

// The default locale context translates every label to an empty string.
jest.mock('@hooks/useLocalize', () => {
const {translateLocal, localeCompare} = jest.requireActual<typeof TestHelper>('../utils/TestHelper');
return () => ({translate: translateLocal, localeCompare});
});

function renderFiltersPage() {
render(
<NavigationContainer>
<SearchAdvancedFiltersBase />
</NavigationContainer>,
);
}

beforeAll(async () => {
await IntlStore.load('en');
});

describe('SearchAdvancedFiltersBase', () => {
it('pins Apply filters below the list by default', () => {
// Given a layout with room for a fixed footer
jest.mocked(useShouldFooterBeInsideList).mockReturnValue(false);

// When the Filters page renders
renderFiltersPage();

// Then Apply filters is outside the scrolling list
expect(screen.getByText('Apply filters')).toBeOnTheScreen();
expect(within(screen.getByTestId('filter-list')).queryByText('Apply filters')).not.toBeOnTheScreen();
});

it('scrolls Apply filters with the list in landscape with the keyboard open', () => {
// Given landscape mode with the keyboard open, where a fixed footer would push the search input out of view
jest.mocked(useShouldFooterBeInsideList).mockReturnValue(true);

// When the Filters page renders
renderFiltersPage();

// Then Apply filters is rendered once, inside the scrolling list
expect(screen.getAllByText('Apply filters')).toHaveLength(1);
expect(within(screen.getByTestId('filter-list')).getByText('Apply filters')).toBeOnTheScreen();
});
});
Loading