Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-33638) feat(chronicle): add anchor on chronicle when see more button used #7602

Merged
merged 6 commits into from
Feb 3, 2025
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ComponentMeta } from '@storybook/react'
import React from 'react'
import { View } from 'react-native'

import { ChronicleCard } from 'features/chronicle/components/ChronicleCard/ChronicleCard'
import { ButtonTertiaryBlack } from 'ui/components/buttons/ButtonTertiaryBlack'
import { VariantsTemplate, type Variants, type VariantsStory } from 'ui/storybook/VariantsTemplate'

const meta: ComponentMeta<typeof ChronicleCard> = {
Expand All @@ -24,6 +26,17 @@ const variantConfig: Variants<typeof ChronicleCard> = [
label: 'ChronicleCard default',
props: { ...baseProps },
},
{
label: 'ChronicleCard with see more button',
props: {
...baseProps,
children: (
<View>
<ButtonTertiaryBlack wording="Voir plus" />
</View>
),
},
},
]

const Template: VariantsStory<typeof ChronicleCard> = (args) => (
Expand Down
22 changes: 17 additions & 5 deletions src/features/chronicle/components/ChronicleCard/ChronicleCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent } from 'react'
import React, { FunctionComponent, PropsWithChildren } from 'react'
import styled from 'styled-components/native'

import { ChronicleCardData } from 'features/chronicle/type'
Expand All @@ -10,9 +10,11 @@ import { TypoDS, getShadow, getSpacing } from 'ui/theme'

const CHRONICLE_THUMBNAIL_SIZE = getSpacing(14)

type Props = ChronicleCardData & {
cardWidth?: number
}
type Props = PropsWithChildren<
ChronicleCardData & {
cardWidth?: number
}
>

export const ChronicleCard: FunctionComponent<Props> = ({
id,
Expand All @@ -21,6 +23,7 @@ export const ChronicleCard: FunctionComponent<Props> = ({
description,
date,
cardWidth,
children,
}) => {
return (
<Container gap={3} testID={`chronicle-card-${id.toString()}`} width={cardWidth}>
Expand All @@ -32,7 +35,10 @@ export const ChronicleCard: FunctionComponent<Props> = ({
/>
<Separator.Horizontal />
<Description>{description}</Description>
<PublicationDate>{date}</PublicationDate>
<BottomCardContainer>
<PublicationDate>{date}</PublicationDate>
{children}
</BottomCardContainer>
</Container>
)
}
Expand All @@ -58,6 +64,12 @@ const Description = styled(TypoDS.BodyAccentS)(({ theme }) => ({
flexGrow: 1,
}))

const BottomCardContainer = styled.View({
flexDirection: 'row',
justifyContent: 'space-between',
})

const PublicationDate = styled(TypoDS.BodyAccentXs)(({ theme }) => ({
color: theme.colors.greyDark,
alignSelf: 'center',
}))
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import colorAlpha from 'color-alpha'
import React, { FunctionComponent, useRef } from 'react'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { NativeScrollEvent, NativeSyntheticEvent, useWindowDimensions, View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import LinearGradient from 'react-native-linear-gradient'
import { useTheme } from 'styled-components'
import styled from 'styled-components/native'

import { CHRONICLE_CARD_WIDTH } from 'features/chronicle/constant'
import { ChronicleCardData } from 'features/chronicle/type'
import { useHorizontalFlatListScroll } from 'ui/hooks/useHorizontalFlatListScroll'
import { PlaylistArrowButton } from 'ui/Playlist/PlaylistArrowButton'

Expand All @@ -16,21 +17,34 @@ import {
SEPARATOR_DEFAULT_VALUE,
} from './ChronicleCardListBase'

export const ChronicleCardList: FunctionComponent<ChronicleCardListProps> = ({
data,
horizontal = true,
cardWidth,
contentContainerStyle,
headerComponent,
separatorSize = SEPARATOR_DEFAULT_VALUE,
onScroll,
style,
}) => {
export const ChronicleCardList = forwardRef<
Partial<FlatList<ChronicleCardData>>,
ChronicleCardListProps
>(function ChronicleCardList(
{
data,
horizontal = true,
cardWidth,
contentContainerStyle,
onScroll,
headerComponent,
style,
separatorSize = SEPARATOR_DEFAULT_VALUE,
onSeeMoreButtonPress,
onLayout,
},
ref
) {
const { isDesktopViewport } = useTheme()
const { width: windowWidth } = useWindowDimensions()

const listRef = useRef<FlatList>(null)

useImperativeHandle(ref, () => ({
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
scrollToIndex: (params) => listRef.current?.scrollToIndex(params),
}))

const {
onScroll: internalScrollHandler,
handleScrollNext,
Expand Down Expand Up @@ -86,10 +100,12 @@ export const ChronicleCardList: FunctionComponent<ChronicleCardListProps> = ({
separatorSize={separatorSize}
contentContainerStyle={contentContainerStyle}
snapToInterval={isDesktopViewport ? CHRONICLE_CARD_WIDTH : undefined}
onSeeMoreButtonPress={onSeeMoreButtonPress}
onLayout={onLayout}
/>
</View>
)
}
})

const ArrowWrapper = styled.View.attrs({
pointerEvents: 'box-none',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React, { createRef } from 'react'
import { FlatList } from 'react-native-gesture-handler'
import { ReactTestInstance } from 'react-test-renderer'

import { CHRONICLE_CARD_WIDTH } from 'features/chronicle/constant'
import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap'
import { render, screen } from 'tests/utils'
import { render, screen, userEvent } from 'tests/utils'

import { ChronicleCardListBase } from './ChronicleCardListBase'

const user = userEvent.setup()

jest.useFakeTimers()

describe('ChronicleCardListBase', () => {
const ref = createRef<FlatList>()

Expand Down Expand Up @@ -34,4 +39,51 @@ describe('ChronicleCardListBase', () => {

expect(screen.getByText('La Nature Sauvage')).toBeOnTheScreen()
})

it('should display "Voir plus" button on all cards when onPressSeeMoreButton defined', () => {
render(
<ChronicleCardListBase
data={chroniclesSnap}
offset={CHRONICLE_CARD_WIDTH}
horizontal
ref={ref}
onSeeMoreButtonPress={jest.fn()}
/>
)

expect(screen.getAllByText('Voir plus')).toHaveLength(10)
})

it('should not display "Voir plus" button on all cards when onSeeMoreButtonPress not defined', () => {
render(
<ChronicleCardListBase
data={chroniclesSnap}
offset={CHRONICLE_CARD_WIDTH}
horizontal
ref={ref}
/>
)

expect(screen.queryByText('Voir plus')).not.toBeOnTheScreen()
})

it('should handle onSeeMoreButtonPress when pressing "Voir plus" button', async () => {
const mockOnSeeMoreButtonPress = jest.fn()
render(
<ChronicleCardListBase
data={chroniclesSnap}
offset={CHRONICLE_CARD_WIDTH}
horizontal
ref={ref}
onSeeMoreButtonPress={mockOnSeeMoreButtonPress}
/>
)

const seeMoreButtons = screen.getAllByText('Voir plus')

// Using as because links is never undefined and the typing is not correct
await user.press(seeMoreButtons[2] as ReactTestInstance)

expect(mockOnSeeMoreButtonPress).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React, {
ReactElement,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from 'react'
import { FlatList, FlatListProps, StyleProp, ViewStyle } from 'react-native'
import { FlatList, FlatListProps, ListRenderItem, StyleProp, View, ViewStyle } from 'react-native'
import styled from 'styled-components/native'

import { ChronicleCardData } from 'features/chronicle/type'
import { ButtonTertiaryBlack } from 'ui/components/buttons/ButtonTertiaryBlack'
import { styledButton } from 'ui/components/buttons/styledButton'
import { PlainMore } from 'ui/svg/icons/PlainMore'
import { getSpacing } from 'ui/theme'

import { ChronicleCard } from '../ChronicleCard/ChronicleCard'
Expand All @@ -26,25 +30,14 @@ export type ChronicleCardListProps = Pick<
| 'snapToInterval'
| 'onScroll'
| 'onContentSizeChange'
| 'onLayout'
> & {
offset?: number
cardWidth?: number
separatorSize?: number
headerComponent?: ReactElement
style?: StyleProp<ViewStyle>
}

const renderItem = ({ item, cardWidth }: { item: ChronicleCardData; cardWidth?: number }) => {
return (
<ChronicleCard
id={item.id}
title={item.title}
subtitle={item.subtitle}
description={item.description}
date={item.date}
cardWidth={cardWidth}
/>
)
onSeeMoreButtonPress?: (chronicleId: number) => void
}

export const ChronicleCardListBase = forwardRef<
Expand All @@ -63,13 +56,16 @@ export const ChronicleCardListBase = forwardRef<
onContentSizeChange,
style,
separatorSize = SEPARATOR_DEFAULT_VALUE,
onSeeMoreButtonPress,
onLayout,
},
ref
) {
const listRef = useRef<FlatList>(null)

useImperativeHandle(ref, () => ({
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
scrollToIndex: (params) => listRef.current?.scrollToIndex(params),
}))

useEffect(() => {
Expand All @@ -87,13 +83,37 @@ export const ChronicleCardListBase = forwardRef<
[separatorSize, horizontal]
)

const renderItem = useCallback<ListRenderItem<ChronicleCardData>>(
({ item }) => {
return (
<ChronicleCard
id={item.id}
title={item.title}
subtitle={item.subtitle}
description={item.description}
date={item.date}
cardWidth={cardWidth}>
{onSeeMoreButtonPress ? (
<View>
<StyledButtonTertiaryBlack
wording="Voir plus"
onPress={() => onSeeMoreButtonPress(item.id)}
/>
</View>
) : null}
</ChronicleCard>
)
},
[cardWidth, onSeeMoreButtonPress]
)

return (
<FlatList
ref={listRef}
data={data}
style={style}
ListHeaderComponent={headerComponent}
renderItem={({ item }) => renderItem({ item, cardWidth })}
renderItem={renderItem}
keyExtractor={keyExtractor}
ItemSeparatorComponent={Separator}
contentContainerStyle={contentContainerStyle}
Expand All @@ -105,6 +125,17 @@ export const ChronicleCardListBase = forwardRef<
decelerationRate="fast"
snapToInterval={snapToInterval}
testID="chronicle-list"
onLayout={onLayout}
/>
)
})

const StyledPlainMore = styled(PlainMore).attrs(({ theme }) => ({
size: theme.icons.sizes.extraSmall,
}))``

const StyledButtonTertiaryBlack = styledButton(ButtonTertiaryBlack).attrs({
icon: StyledPlainMore,
iconPosition: 'right',
buttonHeight: 'extraSmall',
})``
Loading
Loading