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),
})