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-34395) feat(ChronicleCard): display button when the chronicle is too long #7670

Merged
merged 4 commits into from
Feb 13, 2025
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
@@ -0,0 +1,111 @@
import React from 'react'
import { View } from 'react-native'

import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap'
import { act, render, screen, userEvent } from 'tests/utils'
import { ButtonTertiaryBlack } from 'ui/components/buttons/ButtonTertiaryBlack'

import { ChronicleCard } from './ChronicleCard'

const mockOnLayoutWithButton = {
nativeEvent: {
layout: {
height: 157,
},
},
}

const mockOnLayoutWithoutButton = {
nativeEvent: {
layout: {
height: 30,
},
},
}

const user = userEvent.setup()
const mockOnSeeMoreButtonPress = jest.fn()
jest.useFakeTimers()

describe('ChronicleCard (Mobile)', () => {
it('should render the ChronicleCard component with correct title', () => {
render(
<ChronicleCard {...chroniclesSnap[0]} shouldTruncate>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

expect(screen.getByText(chroniclesSnap[0].title)).toBeOnTheScreen()
})

it('should display the "Voir plus" button if content overflows', async () => {
render(
<ChronicleCard {...chroniclesSnap[0]} shouldTruncate>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

const description = screen.getByTestId('description')

await act(async () => {
description.props.onLayout(mockOnLayoutWithButton)
})

expect(screen.getByText('Voir plus')).toBeOnTheScreen()
})

it('should not display the "Voir plus" button if not content overflows', async () => {
render(
<ChronicleCard {...chroniclesSnap[0]} shouldTruncate>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

const description = screen.getByTestId('description')

await act(async () => {
description.props.onLayout(mockOnLayoutWithoutButton)
})

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

it('should call onSeeMoreButtonPress when "Voir plus" is clicked', async () => {
render(
<ChronicleCard {...chroniclesSnap[0]} shouldTruncate>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

const description = screen.getByTestId('description')

await act(async () => {
description.props.onLayout(mockOnLayoutWithButton)
})

await user.press(screen.getByText('Voir plus'))

expect(mockOnSeeMoreButtonPress).toHaveBeenCalledTimes(1)
expect(mockOnSeeMoreButtonPress).toHaveBeenCalledWith(chroniclesSnap[0].id)
})
})
78 changes: 61 additions & 17 deletions src/features/chronicle/components/ChronicleCard/ChronicleCard.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import React, { FunctionComponent, PropsWithChildren } from 'react'
import styled from 'styled-components/native'
import React, { FunctionComponent, PropsWithChildren, useState } from 'react'
import { LayoutChangeEvent, Platform } from 'react-native'
import styled, { useTheme } from 'styled-components/native'

import { ChronicleCardData } from 'features/chronicle/type'
import { InfoHeader } from 'ui/components/InfoHeader/InfoHeader'
import { Separator } from 'ui/components/Separator'
import { ViewGap } from 'ui/components/ViewGap/ViewGap'
import { BookClubCertification } from 'ui/svg/BookClubCertification'
import { TypoDS, getShadow, getSpacing } from 'ui/theme'
import { REM_TO_PX } from 'ui/theme/constants'

const CHRONICLE_THUMBNAIL_SIZE = getSpacing(14)

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

const MAX_LINES = 3
const CHRONICLE_CARD_HEIGHT = 220

export const ChronicleCard: FunctionComponent<Props> = ({
id,
title,
Expand All @@ -24,7 +30,31 @@ export const ChronicleCard: FunctionComponent<Props> = ({
date,
cardWidth,
children,
shouldTruncate = false,
}) => {
const theme = useTheme()

const [currentNumberOfLines, setCurrentNumberOfLines] = useState<number | undefined>(undefined)
const [shouldDisplayButton, setShouldDisplayButton] = useState(false)

// height depending on the platform
const DEFAULT_HEIGHT_WEB =
parseFloat(theme.designSystem.typography.bodyAccentS.lineHeight) * MAX_LINES * REM_TO_PX
clesausse-pass marked this conversation as resolved.
Show resolved Hide resolved
const DEFAULT_HEIGHT_MOBILE =
parseFloat(theme.designSystem.typography.bodyAccentS.lineHeight) * MAX_LINES
const defaultHeight = Platform.OS === 'web' ? DEFAULT_HEIGHT_WEB : DEFAULT_HEIGHT_MOBILE

const handleOnLayout = (event: LayoutChangeEvent) => {
// We use Math.floor to avoid floating-point precision issues when comparing heights
const actualHeight = Math.floor(event.nativeEvent.layout.height)
const expectedMaxHeight = Math.floor(defaultHeight)

if (actualHeight > expectedMaxHeight) {
setShouldDisplayButton(true)
setCurrentNumberOfLines(3)
}
}

return (
<Container gap={3} testID={`chronicle-card-${id.toString()}`} width={cardWidth}>
<InfoHeader
Expand All @@ -34,29 +64,43 @@ export const ChronicleCard: FunctionComponent<Props> = ({
thumbnailComponent={<BookClubCertification />}
/>
<Separator.Horizontal />
<Description>{description}</Description>
<DescriptionContainer defaultHeight={defaultHeight}>
<Description
testID="description"
onLayout={shouldTruncate ? handleOnLayout : undefined}
numberOfLines={currentNumberOfLines}>
{description}
</Description>
</DescriptionContainer>
<BottomCardContainer>
<PublicationDate>{date}</PublicationDate>
{children}
{shouldDisplayButton && children}
</BottomCardContainer>
</Container>
)
}

const Container = styled(ViewGap)<{ width?: number }>(({ theme, width }) => ({
padding: getSpacing(6),
borderRadius: getSpacing(2),
border: 1,
borderColor: theme.colors.greyMedium,
...(width === undefined ? undefined : { width }),
const Container = styled(ViewGap)<{ width?: number; shouldTruncate?: boolean }>(
({ theme, width, shouldTruncate }) => ({
padding: getSpacing(6),
borderRadius: getSpacing(2),
border: 1,
borderColor: theme.colors.greyMedium,
...(width === undefined ? undefined : { width }),
height: shouldTruncate ? CHRONICLE_CARD_HEIGHT : undefined,
backgroundColor: theme.colors.white,
...getShadow({
shadowOffset: { width: 0, height: getSpacing(1) },
shadowRadius: getSpacing(1),
shadowColor: theme.colors.black,
shadowOpacity: 0.15,
}),
})
)

backgroundColor: theme.colors.white,
...getShadow({
shadowOffset: { width: 0, height: getSpacing(1) },
shadowRadius: getSpacing(1),
shadowColor: theme.colors.black,
shadowOpacity: 0.15,
}),
const DescriptionContainer = styled.View<{ defaultHeight: number }>(({ defaultHeight }) => ({
maxHeight: MAX_LINES * defaultHeight,
overflow: 'hidden',
}))

const Description = styled(TypoDS.BodyAccentS)(({ theme }) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ChronicleCardList = forwardRef<
separatorSize = SEPARATOR_DEFAULT_VALUE,
onSeeMoreButtonPress,
onLayout,
shouldTruncate,
},
ref
) {
Expand Down Expand Up @@ -102,6 +103,7 @@ export const ChronicleCardList = forwardRef<
snapToInterval={isDesktopViewport ? CHRONICLE_CARD_WIDTH : undefined}
onSeeMoreButtonPress={onSeeMoreButtonPress}
onLayout={onLayout}
shouldTruncate={shouldTruncate}
/>
</View>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ import { ReactTestInstance } from 'react-test-renderer'

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

import { ChronicleCardListBase } from './ChronicleCardListBase'

const mockOnLayoutWithButton = {
nativeEvent: {
layout: {
height: 157,
},
},
}

const user = userEvent.setup()

jest.useFakeTimers()
Expand Down Expand Up @@ -40,18 +48,25 @@ describe('ChronicleCardListBase', () => {
expect(screen.getByText('La Nature Sauvage')).toBeOnTheScreen()
})

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

expect(screen.getAllByText('Voir plus')).toHaveLength(10)
const descriptions = screen.getAllByTestId('description')

await act(async () => {
descriptions[0]?.props.onLayout(mockOnLayoutWithButton)
})

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

it('should not display "Voir plus" button on all cards when onSeeMoreButtonPress not defined', () => {
Expand All @@ -76,13 +91,20 @@ describe('ChronicleCardListBase', () => {
horizontal
ref={ref}
onSeeMoreButtonPress={mockOnSeeMoreButtonPress}
shouldTruncate
/>
)

const descriptions = screen.getAllByTestId('description')

await act(async () => {
descriptions[0]?.props.onLayout(mockOnLayoutWithButton)
})

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)
await user.press(seeMoreButtons[0] as ReactTestInstance)

expect(mockOnSeeMoreButtonPress).toHaveBeenCalledTimes(1)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type ChronicleCardListProps = Pick<
headerComponent?: ReactElement
style?: StyleProp<ViewStyle>
onSeeMoreButtonPress?: (chronicleId: number) => void
shouldTruncate?: boolean
}

export const ChronicleCardListBase = forwardRef<
Expand All @@ -58,6 +59,7 @@ export const ChronicleCardListBase = forwardRef<
separatorSize = SEPARATOR_DEFAULT_VALUE,
onSeeMoreButtonPress,
onLayout,
shouldTruncate,
},
ref
) {
Expand Down Expand Up @@ -92,7 +94,8 @@ export const ChronicleCardListBase = forwardRef<
subtitle={item.subtitle}
description={item.description}
date={item.date}
cardWidth={cardWidth}>
cardWidth={cardWidth}
shouldTruncate={shouldTruncate}>
{onSeeMoreButtonPress ? (
<View>
<StyledButtonTertiaryBlack
Expand All @@ -104,7 +107,7 @@ export const ChronicleCardListBase = forwardRef<
</ChronicleCard>
)
},
[cardWidth, onSeeMoreButtonPress]
[cardWidth, onSeeMoreButtonPress, shouldTruncate]
)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export const ChronicleSection = (props: ChronicleSectionProps) => {
</Row>
{subtitle ? <StyledSubtitle>{subtitle}</StyledSubtitle> : null}
</Gutter>
<StyledChronicleCardlist data={data} onSeeMoreButtonPress={onSeeMoreButtonPress} />
<StyledChronicleCardlist
data={data}
onSeeMoreButtonPress={onSeeMoreButtonPress}
shouldTruncate
/>
</View>
) : (
<ChronicleSectionBase {...props} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export const ChronicleSectionBase = ({
<TypoDS.Title3 {...getHeadingAttrs(3)}>{title}</TypoDS.Title3>
{subtitle ? <StyledSubtitle>{subtitle}</StyledSubtitle> : null}
</Gutter>
<StyledChronicleCardlist data={data} onSeeMoreButtonPress={onSeeMoreButtonPress} />
<StyledChronicleCardlist
data={data}
onSeeMoreButtonPress={onSeeMoreButtonPress}
shouldTruncate
/>
<Gutter>
<InternalTouchableLink
as={ButtonSecondaryBlack}
Expand Down
Loading
Loading