Skip to content

Commit

Permalink
(PC-33099) feat(venueMap): filter header v2 (#7278)
Browse files Browse the repository at this point in the history
* (PC-33099) feat(venueMap): filter header v2

* refactor: FilterButton target

* test: improve coverage + fix tests
  • Loading branch information
mmeissonnier-pass authored Nov 26, 2024
1 parent b657b4e commit 7144094
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ exports[`<SearchResults/> should render SearchResults 1`] = `
"paddingLeft": 16,
"paddingRight": 16,
},
{},
],
{
"opacity": 1,
Expand Down Expand Up @@ -625,6 +626,7 @@ exports[`<SearchResults/> should render SearchResults 1`] = `
"paddingLeft": 16,
"paddingRight": 16,
},
{},
],
{
"opacity": 1,
Expand Down Expand Up @@ -721,6 +723,7 @@ exports[`<SearchResults/> should render SearchResults 1`] = `
"paddingLeft": 16,
"paddingRight": 16,
},
{},
],
{
"opacity": 1,
Expand Down Expand Up @@ -817,6 +820,7 @@ exports[`<SearchResults/> should render SearchResults 1`] = `
"paddingLeft": 16,
"paddingRight": 16,
},
{},
],
{
"opacity": 1,
Expand Down Expand Up @@ -905,6 +909,7 @@ exports[`<SearchResults/> should render SearchResults 1`] = `
"paddingLeft": 16,
"paddingRight": 16,
},
{},
],
{
"opacity": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ import { FilterButton } from './FilterButton'

describe('FilterButton', () => {
it('should contains the number of active filters', async () => {
render(<FilterButton activeFilters={2} />)
render(<FilterButton activeFilters={2} navigateTo={{ screen: 'SearchFilter' }} />)

await waitFor(() => {
expect(screen.getByText('2')).toBeOnTheScreen()
})
})

it('should not display badge when there are no active filters', async () => {
render(<FilterButton activeFilters={0} />)
render(<FilterButton activeFilters={0} navigateTo={{ screen: 'SearchFilter' }} />)

await waitFor(() => {
expect(screen.queryByTestId('searchFilterBadge')).not.toBeOnTheScreen()
})
})

it('should navigate with undefined params when pressing filter button', async () => {
render(<FilterButton activeFilters={1} />)
render(<FilterButton activeFilters={1} navigateTo={{ screen: 'SearchFilter' }} />)

const filterButton = screen.getByTestId('searchFilterBadge')
fireEvent.press(filterButton)
Expand All @@ -35,7 +35,7 @@ describe('FilterButton', () => {

describe('Accessibility', () => {
it('should have an accessible label with the number of active filters', async () => {
render(<FilterButton activeFilters={2} />)
render(<FilterButton activeFilters={2} navigateTo={{ screen: 'SearchFilter' }} />)

await waitFor(() => {
expect(
Expand All @@ -45,7 +45,7 @@ describe('FilterButton', () => {
})

it('should have an accessible label with one active filter', async () => {
render(<FilterButton activeFilters={1} />)
render(<FilterButton activeFilters={1} navigateTo={{ screen: 'SearchFilter' }} />)

await waitFor(() => {
expect(
Expand All @@ -55,7 +55,7 @@ describe('FilterButton', () => {
})

it('should have an accessible label without active filter', async () => {
render(<FilterButton activeFilters={0} />)
render(<FilterButton activeFilters={0} navigateTo={{ screen: 'SearchFilter' }} />)

await waitFor(() => {
expect(screen.getByLabelText('Voir tous les filtres')).toBeOnTheScreen()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import styled from 'styled-components/native'
import { plural } from 'libs/plural'
import { Badge } from 'ui/components/Badge'
import { InternalTouchableLink } from 'ui/components/touchableLink/InternalTouchableLink'
import { InternalTouchableLinkProps } from 'ui/components/touchableLink/types'
import { Filter as FilterIcon } from 'ui/svg/icons/Filter'
import { getSpacing } from 'ui/theme'

type Props = {
activeFilters: number
activeFilters?: number
children?: never
navigateTo?: InternalTouchableLinkProps['navigateTo']
}

export const FilterButton: FunctionComponent<Props> = ({ activeFilters }) => {
export const FilterButton: FunctionComponent<Props> = ({ activeFilters = 0, navigateTo }) => {
const accessibilityLabel =
activeFilters > 0
? plural(activeFilters, {
Expand All @@ -23,7 +25,7 @@ export const FilterButton: FunctionComponent<Props> = ({ activeFilters }) => {

return (
<StyledTouchableLink
navigateTo={{ screen: 'SearchFilter' }}
navigateTo={navigateTo}
title={accessibilityLabel}
accessibilityLabel={accessibilityLabel}>
<RoundContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { FunctionComponent, ReactElement } from 'react'
import { AccessibilityProps } from 'react-native'
import styled from 'styled-components/native'

import { styledButton } from 'ui/components/buttons/styledButton'
Expand All @@ -8,23 +9,25 @@ import { customFocusOutline } from 'ui/theme/customFocusOutline/customFocusOutli
import { getHoverStyle } from 'ui/theme/getHoverStyle/getHoverStyle'

type IsSelectedProps = {
isSelected: boolean
isSelected?: boolean
}

type SingleFilterButtonProps = IsSelectedProps & {
label: string
testID?: string
icon?: ReactElement
onPress: () => void
children?: never
}
type SingleFilterButtonProps = IsSelectedProps &
Partial<AccessibilityProps> & {
label: string
testID?: string
icon?: ReactElement
onPress: () => void
children?: never
}

export const SingleFilterButton: FunctionComponent<SingleFilterButtonProps> = ({
label,
isSelected,
onPress,
icon,
testID,
...accessibilityProps
}) => {
const filterButtonLabel = testID ? `${testID}Label` : 'filterButtonLabel'

Expand All @@ -33,7 +36,8 @@ export const SingleFilterButton: FunctionComponent<SingleFilterButtonProps> = ({
<TouchableContainer
isSelected={isSelected}
onPress={onPress}
accessibilityLabel={accessibilityLabel}>
accessibilityLabel={accessibilityLabel}
{...accessibilityProps}>
<Label testID={filterButtonLabel}>{label}</Label>
{icon}
</TouchableContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,10 @@ export const SearchResultsContent: React.FC = () => {
<Spacer.Row numberOfSpaces={5} />
<Ul>
<StyledLi>
<FilterButton activeFilters={activeFiltersCount} />
<FilterButton
activeFilters={activeFiltersCount}
navigateTo={{ screen: 'SearchFilter' }}
/>
</StyledLi>

<StyledLi>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react'

import { FILTERS_VENUE_TYPE_MAPPING } from 'features/venueMap/constant'
import * as useFeatureFlagAPI from 'libs/firebase/firestore/featureFlags/useFeatureFlag'
import { render, screen } from 'tests/utils'

import { FilterBannerContainer } from './FilterBannerContainer'

const useFeatureFlagSpy = jest.spyOn(useFeatureFlagAPI, 'useFeatureFlag')

describe('FilterBannerConainer', () => {
beforeEach(() => {
useFeatureFlagSpy.mockReturnValue(true)
})

it('should render FilterCategoriesBannerContainer if FF is enabled', async () => {
render(<FilterBannerContainer />)
await screen.findAllByTestId(/[A-Z]+Label/)

Object.keys(FILTERS_VENUE_TYPE_MAPPING).forEach((id) => {
expect(screen.getByTestId(`${id}Label`)).toBeOnTheScreen()
})
})

it('should render SingleFilterBannerContainer if FF is disabled', async () => {
useFeatureFlagSpy.mockReturnValueOnce(false)

render(<FilterBannerContainer />)

expect(await screen.findByLabelText('Tous les lieux')).toBeOnTheScreen()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import styled from 'styled-components/native'

import { FILTER_BANNER_HEIGHT } from 'features/venueMap/constant'
import { useFeatureFlag } from 'libs/firebase/firestore/featureFlags/useFeatureFlag'
import { RemoteStoreFeatureFlags } from 'libs/firebase/firestore/types'
import { useGetHeaderHeight } from 'ui/components/headers/PageHeaderWithoutPlaceholder'
import { getSpacing } from 'ui/theme'

import { FilterCategoriesBannerContainer } from './FilterCategoriesBannerContainer'
import { SingleFilterBannerContainer } from './SingleFilterBannerContainer'

export const FilterBannerContainer = () => {
const headerHeight = useGetHeaderHeight()

const filterCategoriesActive = useFeatureFlag(
RemoteStoreFeatureFlags.WIP_VENUE_MAP_TYPE_FILTER_V2
)

return (
<Container headerHeight={headerHeight}>
{filterCategoriesActive ? (
<FilterCategoriesBannerContainer />
) : (
<SingleFilterBannerContainer />
)}
</Container>
)
}

const Container = styled.View<{ headerHeight: number }>(({ headerHeight }) => ({
height: FILTER_BANNER_HEIGHT,
position: 'absolute',
zIndex: 1,
top: headerHeight,
left: 0,
right: 0,
paddingHorizontal: getSpacing(6),
paddingVertical: getSpacing(1),
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'

import { FILTERS_VENUE_TYPE_MAPPING } from 'features/venueMap/constant'
import { render, screen, userEvent } from 'tests/utils'

import { FilterCategoriesBannerContainer } from './FilterCategoriesBannerContainer'

describe('FilterCategoriesBannerContainer', () => {
const user = userEvent.setup()

it('should render correctly', async () => {
render(<FilterCategoriesBannerContainer />)

await screen.findAllByTestId(/[A-Z]+Label/)

Object.keys(FILTERS_VENUE_TYPE_MAPPING).forEach((id) => {
expect(screen.getByTestId(`${id}Label`)).toBeOnTheScreen()
})
})

it.each([
{ id: 'OUTINGS', label: 'Sorties : Filtre sélectionné' },
{ id: 'SHOPS', label: 'Boutiques : Filtre sélectionné' },
{ id: 'OTHERS', label: 'Autres : Filtre sélectionné' },
])(`should select $id group`, async ({ id, label }) => {
jest.useFakeTimers()
render(<FilterCategoriesBannerContainer />)

await user.press(screen.getByTestId(`${id}Label`))

expect(screen.getByLabelText(label)).toBeOnTheScreen()

jest.useRealTimers()
})

it('should deselect selected group', async () => {
jest.useFakeTimers()
render(<FilterCategoriesBannerContainer />)

await user.press(screen.getByTestId('OUTINGSLabel'))

await user.press(await screen.findByLabelText('Sorties : Filtre sélectionné'))

expect(screen.queryByLabelText('Sorties : Filtre sélectionné')).not.toBeOnTheScreen()

jest.useRealTimers()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import colorAlpha from 'color-alpha'
import React from 'react'
import { View } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import styled from 'styled-components/native'

import { FilterButton } from 'features/search/components/Buttons/FilterButton/FilterButton'
import { SingleFilterButton } from 'features/search/components/Buttons/SingleFilterButton/SingleFilterButton'
import { FILTERS_VENUE_TYPE_MAPPING } from 'features/venueMap/constant'
import { useVenueMapFilters } from 'features/venueMap/hook/useVenueMapFilters'
import { AccessibilityRole } from 'libs/accessibilityRole/accessibilityRole'
import { theme } from 'theme'
import { getSpacing } from 'ui/theme'

const BULLET_SIZE = 12

type FilterGroupKey = keyof typeof FILTERS_VENUE_TYPE_MAPPING

type FilterGroupData = {
id: FilterGroupKey
label: string
color: string
}
const filterGroups: FilterGroupData[] = [
{ id: 'OUTINGS', label: 'Sorties', color: theme.colors.coral },
{ id: 'SHOPS', label: 'Boutiques', color: theme.colors.primary },
{ id: 'OTHERS', label: 'Autres', color: theme.colors.skyBlue },
]

export const FilterCategoriesBannerContainer = () => {
const { getSelectedMacroFilters, addMacroFilter, removeMacroFilter } = useVenueMapFilters()

const selectedGroups = getSelectedMacroFilters()

const handleCategoryPress = (categoryId: FilterGroupKey) => {
if (selectedGroups.includes(categoryId)) {
removeMacroFilter(categoryId)
} else {
addMacroFilter(categoryId)
}
}

return (
<Container accessibilityRole={AccessibilityRole.LIST}>
<FilterButton />
{filterGroups.map(({ color, id, label }) => (
<SingleFilterButton
key={id}
testID={id}
icon={<ColoredGradientBullet color={color} />}
label={label}
isSelected={selectedGroups.includes(id)}
onPress={() => handleCategoryPress(id)}
accessibilityRole={AccessibilityRole.LISTITEM}
/>
))}
</Container>
)
}

const Container = styled(View)({
flexDirection: 'row',
columnGap: getSpacing(1),
})

const ColoredGradientBullet = styled(LinearGradient).attrs(({ color }: { color: string }) => ({
colors: [color, colorAlpha(color, 0.7)],
}))<{ color: string }>({
backgroundColor: 'black',
width: BULLET_SIZE,
height: BULLET_SIZE,
borderRadius: BULLET_SIZE * 0.5,
})
Loading

0 comments on commit 7144094

Please sign in to comment.