Skip to content

Commit a4a40c5

Browse files
authored
[IRN-6179] [BpkCardList] Add initiallyInViewCardIndex prop with page-based scroll support (#4221)
* implement initiallyInViewCardIndex * fix tests * attach refs to placeholder elements too * useState istead of useMemo for initialPageIndex * storybook example
1 parent 983b814 commit a4a40c5

11 files changed

Lines changed: 368 additions & 28 deletions

File tree

examples/bpk-component-card-list/examples.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,19 @@ const RowToRailWithoutTitleExample = () => (
352352
</PageContainer>
353353
);
354354

355+
const RowToRailWithInitiallyInViewCardIndexExample = () => (
356+
<PageContainer>
357+
<BpkCardList
358+
{...commonProps}
359+
cardList={makeList(DestinationCard)}
360+
layoutDesktop={LAYOUTS.row}
361+
layoutMobile={LAYOUTS.rail}
362+
accessoryDesktop={ACCESSORY_DESKTOP_TYPES.pagination}
363+
initiallyInViewCardIndex={7}
364+
/>
365+
</PageContainer>
366+
);
367+
355368
const MultiComponentsScrollingTestExample = () => (
356369
<PageContainer>
357370
<RowToRailExample />
@@ -386,5 +399,6 @@ export {
386399
GridToStackWithExpandExample,
387400
RowToRailForSnippetsExample,
388401
RowToRailWithoutTitleExample,
402+
RowToRailWithInitiallyInViewCardIndexExample,
389403
MultiComponentsScrollingTestExample,
390404
};

examples/bpk-component-card-list/stories.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
GridToStackWithExpandExample,
2929
RowToRailForSnippetsExample,
3030
RowToRailWithoutTitleExample,
31+
RowToRailWithInitiallyInViewCardIndexExample,
3132
MultiComponentsScrollingTestExample,
3233
} from './examples';
3334

@@ -45,6 +46,7 @@ export const RowToStackWithExpand = RowToStackWithExpandExample;
4546
export const GridToStackWithExpand = GridToStackWithExpandExample;
4647
export const RowToRailForSnippets = RowToRailForSnippetsExample;
4748
export const RowToRailWithoutTitle = RowToRailWithoutTitleExample;
49+
export const RowToRailWithInitiallyInViewCardIndex = RowToRailWithInitiallyInViewCardIndexExample;
4850

4951
export const MultiComponentsScrollingTest = MultiComponentsScrollingTestExample;
5052
export const VisualTest = Basic;

packages/bpk-component-card-list/src/BpkCardList.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const BpkCardList = (props: CardListProps) => {
4848
chipGroup,
4949
description,
5050
expandText,
51+
initiallyInViewCardIndex = 0,
5152
initiallyShownCardsDesktop = DEFAULT_ITEMS_DESKTOP,
5253
initiallyShownCardsMobile = DEFAULT_ITEMS_MOBILE,
5354
layoutDesktop,
@@ -105,6 +106,7 @@ const BpkCardList = (props: CardListProps) => {
105106
initiallyShownCards={initiallyShownCardsMobile}
106107
layout={layoutMobile}
107108
accessibilityLabels={accessibilityLabels}
109+
initiallyInViewCardIndex={initiallyInViewCardIndex}
108110
isMobile
109111
>
110112
{cardList}
@@ -135,6 +137,7 @@ const BpkCardList = (props: CardListProps) => {
135137
initiallyShownCards={initiallyShownCardsDesktop}
136138
layout={layoutDesktop}
137139
accessibilityLabels={accessibilityLabels}
140+
initiallyInViewCardIndex={initiallyInViewCardIndex}
138141
>
139142
{cardList}
140143
</BpkCardListRowRailContainer>
@@ -164,4 +167,4 @@ const BpkCardList = (props: CardListProps) => {
164167
);
165168
};
166169

167-
export default BpkCardList;
170+
export default BpkCardList;

packages/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListCarousel-test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('BpkCardListCarousel', () => {
6464
initiallyShownCards: 3,
6565
layout: LAYOUTS.row,
6666
setCurrentIndex: mockSetCurrentIndex,
67+
initialPageIndex: 0,
6768
};
6869

6970
afterEach(() => {

packages/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListCarousel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => {
5353
`Entering Carousel with ${initiallyShownCards} slides shown at a time, ${childrenLength} slides in total. Please use Pagination below with the Previous and Next buttons to navigate, or the slide dot buttons at the end to jump to slides.`,
5454
children,
5555
currentIndex,
56+
initialPageIndex,
5657
initiallyShownCards,
5758
isMobile = false,
5859
layout,
@@ -94,6 +95,7 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => {
9495
visibilityList,
9596
container: root,
9697
enabled: !isMobile,
98+
initialPageIndex,
9799
});
98100

99101
// Similar to Virtual Scrolling to improve performance
@@ -230,6 +232,7 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => {
230232
return (
231233
<div
232234
{...commonProps}
235+
ref={cardRefFns[index]}
233236
style={{
234237
...commonProps.style,
235238
...cardDimensionStyle,

packages/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListRowRailContainer-test.tsx

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,14 @@ describe('BpkCardListRowRailContainer', () => {
4646

4747
it('should render correctly with row layout and no accessory', () => {
4848
render(
49-
<BpkCardListRowRailContainer
50-
layout={LAYOUTS.row}
51-
initiallyShownCards={3}
52-
>
49+
<BpkCardListRowRailContainer layout={LAYOUTS.row} initiallyShownCards={3} initiallyInViewCardIndex={0}>
5350
{mockCards(3)}
5451
</BpkCardListRowRailContainer>,
5552
);
5653

57-
const pagination = screen.queryByTestId('bpk-card-list-row-rail__accessory');
54+
const pagination = screen.queryByTestId(
55+
'bpk-card-list-row-rail__accessory',
56+
);
5857
const container = screen.getByTestId('bpk-card-list-row-rail');
5958
const carousel = screen.getByTestId('bpk-card-list-row-rail__carousel');
6059

@@ -69,17 +68,132 @@ describe('BpkCardListRowRailContainer', () => {
6968
layout={LAYOUTS.row}
7069
initiallyShownCards={3}
7170
accessory={ACCESSORY_DESKTOP_TYPES.pagination}
71+
initiallyInViewCardIndex={0}
7272
>
7373
{mockCards(5)}
7474
</BpkCardListRowRailContainer>,
7575
);
7676

77-
const pagination = screen.queryByTestId('bpk-card-list-row-rail__accessory');
77+
const pagination = screen.queryByTestId(
78+
'bpk-card-list-row-rail__accessory',
79+
);
7880
const container = screen.getByTestId('bpk-card-list-row-rail');
7981
const carousel = screen.getByTestId('bpk-card-list-row-rail__carousel');
8082

8183
expect(pagination).toBeInTheDocument();
8284
expect(container).toBeInTheDocument();
8385
expect(carousel).toBeInTheDocument();
8486
});
87+
88+
describe('initiallyInViewCardIndex', () => {
89+
const mockScrollIntoView = jest.fn();
90+
const originalScrollIntoView = Element.prototype.scrollIntoView;
91+
92+
beforeAll(() => {
93+
Element.prototype.scrollIntoView = mockScrollIntoView;
94+
});
95+
96+
afterAll(() => {
97+
Element.prototype.scrollIntoView = originalScrollIntoView;
98+
});
99+
100+
beforeEach(() => {
101+
mockScrollIntoView.mockClear();
102+
});
103+
104+
it('should scroll to the first card of the target page on mount', () => {
105+
render(
106+
<BpkCardListRowRailContainer
107+
layout={LAYOUTS.row}
108+
initiallyShownCards={3}
109+
accessory={ACCESSORY_DESKTOP_TYPES.pagination}
110+
initiallyInViewCardIndex={4}
111+
>
112+
{mockCards(9)}
113+
</BpkCardListRowRailContainer>,
114+
);
115+
116+
const cards = screen.getAllByRole('group');
117+
118+
// Card index 4 → page 1 → scrolls to card at index 3 (page 1 start)
119+
expect(mockScrollIntoView).toHaveBeenCalledTimes(1);
120+
expect(mockScrollIntoView.mock.instances[0]).toBe(cards[3]);
121+
});
122+
123+
it('should not trigger initial scroll when initiallyInViewCardIndex is 0', () => {
124+
render(
125+
<BpkCardListRowRailContainer
126+
layout={LAYOUTS.row}
127+
initiallyShownCards={3}
128+
initiallyInViewCardIndex={0}
129+
>
130+
{mockCards(6)}
131+
</BpkCardListRowRailContainer>,
132+
);
133+
134+
expect(mockScrollIntoView).not.toHaveBeenCalled();
135+
});
136+
137+
it('should not trigger initial scroll when initiallyInViewCardIndex is negative', () => {
138+
render(
139+
<BpkCardListRowRailContainer
140+
layout={LAYOUTS.row}
141+
initiallyShownCards={3}
142+
initiallyInViewCardIndex={-1}
143+
>
144+
{mockCards(6)}
145+
</BpkCardListRowRailContainer>,
146+
);
147+
148+
expect(mockScrollIntoView).not.toHaveBeenCalled();
149+
});
150+
151+
it('should scroll to the last page start when initiallyInViewCardIndex exceeds total cards', () => {
152+
render(
153+
<BpkCardListRowRailContainer
154+
layout={LAYOUTS.row}
155+
initiallyShownCards={3}
156+
accessory={ACCESSORY_DESKTOP_TYPES.pagination}
157+
initiallyInViewCardIndex={100}
158+
>
159+
{mockCards(6)}
160+
</BpkCardListRowRailContainer>,
161+
);
162+
163+
const cards = screen.getAllByRole('group');
164+
165+
// Clamped to last page (page 1) → scrolls to card at index 3
166+
expect(mockScrollIntoView).toHaveBeenCalledTimes(1);
167+
expect(mockScrollIntoView.mock.instances[0]).toBe(cards[3]);
168+
});
169+
170+
it.each([
171+
{ cardIndex: 3, shownCards: 3, totalCards: 12, expectedPage: 1 },
172+
{ cardIndex: 5, shownCards: 3, totalCards: 12, expectedPage: 1 },
173+
{ cardIndex: 11, shownCards: 3, totalCards: 12, expectedPage: 3 },
174+
{ cardIndex: 8, shownCards: 4, totalCards: 12, expectedPage: 2 },
175+
])(
176+
'should scroll to card at page start for cardIndex=$cardIndex with shownCards=$shownCards (page $expectedPage)',
177+
({ cardIndex, expectedPage, shownCards, totalCards }) => {
178+
render(
179+
<BpkCardListRowRailContainer
180+
layout={LAYOUTS.row}
181+
initiallyShownCards={shownCards}
182+
accessory={ACCESSORY_DESKTOP_TYPES.pagination}
183+
initiallyInViewCardIndex={cardIndex}
184+
>
185+
{mockCards(totalCards)}
186+
</BpkCardListRowRailContainer>,
187+
);
188+
189+
const cards = screen.getAllByRole('group');
190+
const expectedCardIndex = expectedPage * shownCards;
191+
192+
expect(mockScrollIntoView).toHaveBeenCalledTimes(1);
193+
expect(mockScrollIntoView.mock.instances[0]).toBe(
194+
cards[expectedCardIndex],
195+
);
196+
},
197+
);
198+
});
85199
});

packages/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListRowRailContainer.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const BpkCardListRowRailContainer = (props: CardListRowRailProps) => {
3434
accessibilityLabels,
3535
accessory,
3636
children,
37+
initiallyInViewCardIndex,
3738
initiallyShownCards,
3839
isMobile = false,
3940
layout,
@@ -43,7 +44,18 @@ const BpkCardListRowRailContainer = (props: CardListRowRailProps) => {
4344
const totalIndicators = Math.ceil(childrenCount / initiallyShownCards);
4445
const showAccessory = childrenCount > initiallyShownCards;
4546

46-
const [currentIndex, setCurrentIndex] = useState(0);
47+
// Calculate initial page from card index
48+
const [initialPageIndex] = useState(() => {
49+
if (initiallyInViewCardIndex < 0) {
50+
return 0;
51+
}
52+
if (initiallyInViewCardIndex >= childrenCount) {
53+
return Math.max(0, totalIndicators - 1);
54+
}
55+
return Math.floor(initiallyInViewCardIndex / initiallyShownCards);
56+
});
57+
58+
const [currentIndex, setCurrentIndex] = useState(initialPageIndex);
4759

4860
const accessoryContent =
4961
layout === LAYOUTS.row &&
@@ -73,6 +85,7 @@ const BpkCardListRowRailContainer = (props: CardListRowRailProps) => {
7385
isMobile={isMobile}
7486
carouselLabel={accessibilityLabels?.carouselLabel}
7587
slideLabel={accessibilityLabels?.slideLabel}
88+
initialPageIndex={initialPageIndex}
7689
>
7790
{children}
7891
</BpkCardListCarousel>
@@ -91,4 +104,4 @@ const BpkCardListRowRailContainer = (props: CardListRowRailProps) => {
91104
);
92105
};
93106

94-
export default BpkCardListRowRailContainer;
107+
export default BpkCardListRowRailContainer;

packages/bpk-component-card-list/src/BpkCardListRowRail/accessibility-test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ describe('BpkCardListRowRailContainer', () => {
4444
}
4545
};
4646
});
47-
47+
4848
it('should have no accessibility issues for row and no accessory', async () => {
4949
const { container } = render(
50-
<BpkCardListRowRailContainer layout={LAYOUTS.row} initiallyShownCards={3}>
50+
<BpkCardListRowRailContainer layout={LAYOUTS.row} initiallyShownCards={3} initiallyInViewCardIndex={0}>
5151
{mockCards(3)}
5252
</BpkCardListRowRailContainer>,
5353
);
@@ -62,6 +62,7 @@ describe('BpkCardListRowRailContainer', () => {
6262
layout={LAYOUTS.row}
6363
initiallyShownCards={3}
6464
accessory="pagination"
65+
initiallyInViewCardIndex={0}
6566
>
6667
{mockCards(5)}
6768
</BpkCardListRowRailContainer>,

0 commit comments

Comments
 (0)