diff --git a/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.test.tsx b/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.test.tsx index 7ac86e77b1e..694d8028468 100644 --- a/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.test.tsx +++ b/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.test.tsx @@ -1,33 +1,91 @@ import React from 'react' +import { ChronicleCardListProps } from 'features/chronicle/components/ChronicleCardList/ChronicleCardListBase' import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap' import { fireEvent, render, screen, waitFor } from 'tests/utils/web' import { ChronicleCardList } from './ChronicleCardList' describe('ChronicleCardList', () => { - it('should render the ChronicleCardList correctly', () => { - render() + let mockCallback: ( + entries: { + target: HTMLElement + contentRect: Partial + }[] + ) => void + + beforeAll(() => { + const mockResizeObserver = jest.fn().mockImplementation((callback) => { + mockCallback = callback + return { + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), + } + }) + + global.ResizeObserver = mockResizeObserver + }) + + const forceOnLayout = () => { + // Simuler un changement de taille + const list = screen.getByTestId('chronicle-list') + const listParent = list.parentElement + + if (!listParent) { + return + } + + Object.defineProperties(listParent, { + offsetHeight: { value: 400 }, + offsetWidth: { value: 4000 }, + }) + + Object.defineProperties(list, { + scrollWidth: { value: 4000 }, + offsetWidth: { value: 400 }, + }) + + mockCallback([ + { + target: listParent, + contentRect: { width: 4000, height: 400, top: 0, left: 0 }, + }, + ]) + } + + it('should render the ChronicleCardList correctly', async () => { + renderChronicleList() + forceOnLayout() + + await screen.findByTestId('chronicle-list-right-arrow') expect(screen.getByText('Le Voyage Extraordinaire')).toBeInTheDocument() expect(screen.getByText('L’Art de la Cuisine')).toBeInTheDocument() }) - it('should render the ChronicleCardList with horizontal mode', () => { - render() + it('should render the ChronicleCardList in horizontal mode', async () => { + renderChronicleList() + forceOnLayout() - expect(screen.getByTestId('chronicle-list-right-arrow')).toBeInTheDocument() + expect(await screen.findByTestId('chronicle-list-right-arrow')).toBeInTheDocument() }) - it('should render the ChronicleCardList with vertical mode', () => { - render() + it('should render the ChronicleCardList in vertical mode', async () => { + renderChronicleList({ horizontal: false }) + forceOnLayout() - expect(screen.queryByTestId('chronicle-list-left-arrow')).not.toBeInTheDocument() - expect(screen.queryByTestId('chronicle-list-right-arrow')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.queryByTestId('chronicle-list-left-arrow')).not.toBeInTheDocument() + expect(screen.queryByTestId('chronicle-list-right-arrow')).not.toBeInTheDocument() + }) }) - it('should go to next page when right arrow is pressed', () => { - render() + it('should go to next page when right arrow is pressed', async () => { + renderChronicleList() + forceOnLayout() + + await screen.findByTestId('chronicle-list-right-arrow') fireEvent.click(screen.getByTestId('chronicle-list-right-arrow')) @@ -38,19 +96,18 @@ describe('ChronicleCardList', () => { }) it('should go to previous page when left arrow is pressed', async () => { - render() + renderChronicleList() + forceOnLayout() const listElement = screen.getByTestId('chronicle-list') - Object.defineProperty(listElement, 'scrollWidth', { get: () => 900 }) - Object.defineProperty(listElement, 'offsetWidth', { get: () => 300 }) + + await screen.findByTestId('chronicle-list-right-arrow') fireEvent.click(screen.getByTestId('chronicle-list-right-arrow')) // We have to force scroll event. onScroll is not triggered when using scrollToOffset via ref fireEvent.scroll(listElement) - await screen.findByTestId('chronicle-list-left-arrow') - - fireEvent.click(screen.getByTestId('chronicle-list-left-arrow')) + fireEvent.click(await screen.findByTestId('chronicle-list-left-arrow')) fireEvent.scroll(listElement) await waitFor(() => { @@ -59,22 +116,23 @@ describe('ChronicleCardList', () => { }) }) - it('should disable the left arrow when on the first item', () => { - render() + it('should disable the left arrow when on the first item', async () => { + renderChronicleList() + forceOnLayout() + + await screen.findByTestId('chronicle-list-right-arrow') // Ensure that the left arrow is not clickable on the first item expect(screen.queryByTestId('chronicle-list-left-arrow')).not.toBeInTheDocument() }) it('should disable the right arrow when on the last item', async () => { - render() + renderChronicleList({ data: chroniclesSnap.slice(0, 2) }) const listElement = screen.getByTestId('chronicle-list') - Object.defineProperty(listElement, 'scrollWidth', { get: () => 900 }) - Object.defineProperty(listElement, 'offsetWidth', { get: () => 300 }) fireEvent.scroll(listElement, { - target: { scrollLeft: 600 }, + target: { scrollLeft: 3600 }, }) await waitFor(() => @@ -82,3 +140,10 @@ describe('ChronicleCardList', () => { ) }) }) + +const renderChronicleList = (props?: Partial) => { + render() + + const listElement = screen.getByTestId('chronicle-list') + fireEvent.scroll(listElement) +} diff --git a/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.tsx b/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.tsx index 87bd2023d44..fc95961cb4b 100644 --- a/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.tsx +++ b/src/features/chronicle/components/ChronicleCardList/ChronicleCardList.web.tsx @@ -1,14 +1,11 @@ -import React, { FunctionComponent, useState } from 'react' -import { - LayoutChangeEvent, - LayoutRectangle, - NativeScrollEvent, - NativeSyntheticEvent, -} from 'react-native' +import React, { FunctionComponent, useRef } from 'react' +import { useWindowDimensions } from 'react-native' +import { FlatList } from 'react-native-gesture-handler' import { useTheme } from 'styled-components' import styled from 'styled-components/native' import { CHRONICLE_CARD_WIDTH } from 'features/chronicle/constant' +import { useHorizontalFlatListScroll } from 'ui/hooks/useHorizontalFlatListScroll' import { PlaylistArrowButton } from 'ui/Playlist/PlaylistArrowButton' import { @@ -25,70 +22,52 @@ export const ChronicleCardList: FunctionComponent = ({ headerComponent, separatorSize = SEPARATOR_DEFAULT_VALUE, }) => { - const [userOffset, setUserOffset] = useState(0) - const [scrollOffset, setScrollOffset] = useState(0) - const [layout, setLayout] = useState() - const [leftArrowVisible, setLeftArrowVisible] = useState(false) - const [rightArrowVisible, setRightArrowVisible] = useState(true) - const { isDesktopViewport } = useTheme() + const { width: windowWidth } = useWindowDimensions() - const pageWidth = isDesktopViewport ? layout?.width ?? 0 : CHRONICLE_CARD_WIDTH - const goToPreviousPage = () => setUserOffset(Math.max(scrollOffset - pageWidth, 0)) - const goToNextPage = () => setUserOffset(scrollOffset + pageWidth) - - const handleLayout = (event: LayoutChangeEvent) => { - setLayout(event.nativeEvent.layout) - } - - const handleScroll = (event: NativeSyntheticEvent) => { - const { contentSize, layoutMeasurement, contentOffset } = event.nativeEvent - const progress = contentOffset.x / (contentSize.width - layoutMeasurement.width) - - setScrollOffset(contentOffset.x) + const listRef = useRef(null) - switch (progress) { - case 0: - setLeftArrowVisible(false) - setRightArrowVisible(true) - break - case 1: - setLeftArrowVisible(true) - setRightArrowVisible(false) - break - default: - setLeftArrowVisible(true) - setRightArrowVisible(true) - } - } + const { + onScroll, + handleScrollNext, + handleScrollPrevious, + onContainerLayout, + isEnd, + isStart, + onContentSizeChange, + } = useHorizontalFlatListScroll({ + ref: listRef, + scrollRatio: isDesktopViewport ? 1 : (cardWidth ?? CHRONICLE_CARD_WIDTH) / windowWidth, + }) return ( - + {horizontal ? ( - {leftArrowVisible ? ( + {isStart ? null : ( - ) : null} + )} - {rightArrowVisible ? ( + {isEnd ? null : ( - ) : null} + )} ) : null} { + const ref = createRef() + it('should display all chronicle cards in the list in horizontal', () => { - render() + render() expect(screen.getByText('Le Voyage Extraordinaire')).toBeOnTheScreen() }) it('should display all chronicle cards in the list in vertical', () => { - render() + render() expect(screen.getByText('Le Voyage Extraordinaire')).toBeOnTheScreen() }) it('should scroll to the correct page when offset is provided', () => { - render() + render( + + ) expect(screen.getByText('La Nature Sauvage')).toBeOnTheScreen() }) diff --git a/src/features/chronicle/components/ChronicleCardList/ChronicleCardListBase.tsx b/src/features/chronicle/components/ChronicleCardList/ChronicleCardListBase.tsx index 31a02066bb5..97d9741eaa5 100644 --- a/src/features/chronicle/components/ChronicleCardList/ChronicleCardListBase.tsx +++ b/src/features/chronicle/components/ChronicleCardList/ChronicleCardListBase.tsx @@ -1,11 +1,12 @@ -import React, { FunctionComponent, ReactElement, useEffect, useMemo, useRef } from 'react' -import { - FlatList, - NativeScrollEvent, - NativeSyntheticEvent, - StyleProp, - ViewStyle, -} from 'react-native' +import React, { + ReactElement, + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from 'react' +import { FlatList, FlatListProps } from 'react-native' import styled from 'styled-components/native' import { ChronicleCardData } from 'features/chronicle/type' @@ -17,17 +18,19 @@ export const SEPARATOR_DEFAULT_VALUE = 2 const keyExtractor = (item: ChronicleCardData) => item.id.toString() -export type ChronicleCardListProps = { - data: ChronicleCardData[] +export type ChronicleCardListProps = Pick< + FlatListProps, + | 'data' + | 'contentContainerStyle' + | 'horizontal' + | 'snapToInterval' + | 'onScroll' + | 'onContentSizeChange' +> & { offset?: number - horizontal?: boolean cardWidth?: number - contentContainerStyle?: StyleProp - snapToInterval?: number - scrollEnabled?: boolean separatorSize?: number headerComponent?: ReactElement - onScroll?: (event: NativeSyntheticEvent) => void } const renderItem = ({ item, cardWidth }: { item: ChronicleCardData; cardWidth?: number }) => { @@ -43,20 +46,30 @@ const renderItem = ({ item, cardWidth }: { item: ChronicleCardData; cardWidth?: ) } -export const ChronicleCardListBase: FunctionComponent = ({ - data, - offset, - horizontal = true, - cardWidth, - contentContainerStyle, - onScroll, - snapToInterval, - scrollEnabled, - headerComponent, - separatorSize = SEPARATOR_DEFAULT_VALUE, -}) => { +export const ChronicleCardListBase = forwardRef< + Partial>, + ChronicleCardListProps +>(function ChronicleCardListBase( + { + data, + offset, + horizontal = true, + cardWidth, + contentContainerStyle, + onScroll, + snapToInterval, + headerComponent, + onContentSizeChange, + separatorSize = SEPARATOR_DEFAULT_VALUE, + }, + ref +) { const listRef = useRef(null) + useImperativeHandle(ref, () => ({ + scrollToOffset: (params) => listRef.current?.scrollToOffset(params), + })) + useEffect(() => { if (listRef.current && offset !== undefined) { listRef.current.scrollToOffset({ offset, animated: true }) @@ -81,14 +94,14 @@ export const ChronicleCardListBase: FunctionComponent = keyExtractor={keyExtractor} ItemSeparatorComponent={Separator} contentContainerStyle={contentContainerStyle} + onContentSizeChange={onContentSizeChange} showsHorizontalScrollIndicator={false} onScroll={onScroll} scrollEventThrottle={100} - scrollEnabled={scrollEnabled} horizontal={horizontal} decelerationRate="fast" snapToInterval={snapToInterval} testID="chronicle-list" /> ) -} +}) diff --git a/src/ui/components/SectionWithDivider.tsx b/src/ui/components/SectionWithDivider.tsx index c1540e74ace..29b31ab6564 100644 --- a/src/ui/components/SectionWithDivider.tsx +++ b/src/ui/components/SectionWithDivider.tsx @@ -28,7 +28,7 @@ export const SectionWithDivider = ({ return ( - {margin ? {children} : children} + {margin ? {children} : children} ) } @@ -38,6 +38,6 @@ const Divider = styled.View(({ theme }) => ({ backgroundColor: theme.colors.greyLight, })) -const MarginContainer = styled.View({ +const Wrapper = styled.View({ paddingHorizontal: getSpacing(6), })