-
Notifications
You must be signed in to change notification settings - Fork 7
feat(design-system): add infiniteScroll to DsTable to handle scroll pagination [AR-62554]
#465
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
Merged
iromanchuk-dn
merged 7 commits into
drivenets:main
from
iromanchuk-dn:AR-62554-ds-table-infinite-scrolling-encapsulate
May 13, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ae7854c
feat(design-system): add `infiniteScroll` prop to `DsTable`, letting …
iromanchuk-dn 6db9d92
Merge branch 'main' into AR-62554-ds-table-infinite-scrolling-encapsu…
iromanchuk-dn efc2ba0
Remove latch hack
iromanchuk-dn 3acd321
Merge branch 'main' into AR-62554-ds-table-infinite-scrolling-encapsu…
iromanchuk-dn d521990
update comment
iromanchuk-dn 38fd7a1
Combine multiple ifs into 1
iromanchuk-dn 8bc7c4d
Merge branch 'main' into AR-62554-ds-table-infinite-scrolling-encapsu…
iromanchuk-dn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@drivenets/design-system': minor | ||
| --- | ||
|
|
||
| Add `infiniteScroll` prop to `DsTable`, letting it handle viewport/scroll and auto-fill pagination while consumers manage data and loading state. |
158 changes: 158 additions & 0 deletions
158
...design-system/src/components/ds-table/__tests__/ds-table-infinite-scroll.browser.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
| import { page } from 'vitest/browser'; | ||
| import DsTable from '../ds-table'; | ||
| import { columns, type Person } from '../stories/common/story-data'; | ||
|
|
||
| const statuses: Person['status'][] = ['single', 'relationship', 'complicated']; | ||
|
|
||
| function generateTestData(count: number): Person[] { | ||
| return Array.from({ length: count }, (_, i) => ({ | ||
| id: String(i + 1), | ||
| firstName: `First${String(i + 1)}`, | ||
| lastName: `Last${String(i + 1)}`, | ||
| age: 20 + (i % 50), | ||
| visits: i * 10, | ||
| status: statuses[i % statuses.length] ?? 'single', | ||
| progress: i % 100, | ||
| })); | ||
| } | ||
|
|
||
| const getScrollContainer = (): HTMLElement => { | ||
| const el = document.querySelector<HTMLElement>('[class*="virtualizedContainer"]'); | ||
| if (!el) { | ||
| throw new Error('Expected virtualized scroll container'); | ||
| } | ||
| return el; | ||
| }; | ||
|
|
||
| describe('DsTable infinite scroll', () => { | ||
| it('calls onLoadMore when scrolled within thresholdPx of bottom', async () => { | ||
| const onLoadMore = vi.fn(); | ||
| const tallData = generateTestData(200); | ||
|
|
||
| await page.render( | ||
| <div style={{ height: '400px' }}> | ||
| <DsTable | ||
| columns={columns} | ||
| data={tallData} | ||
| virtualized | ||
| infiniteScroll={{ | ||
| hasMore: true, | ||
| isLoadingMore: false, | ||
| onLoadMore, | ||
| thresholdPx: 200, | ||
| autoFill: false, | ||
| }} | ||
| /> | ||
| </div>, | ||
| ); | ||
|
|
||
| const container = getScrollContainer(); | ||
| container.scrollTop = container.scrollHeight - container.clientHeight - 100; | ||
|
|
||
| await expect.poll(() => onLoadMore.mock.calls.length, { timeout: 2000 }).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it('does not call onLoadMore when hasMore is false', async () => { | ||
| const onLoadMore = vi.fn(); | ||
| const tallData = generateTestData(200); | ||
|
|
||
| await page.render( | ||
| <div style={{ height: '400px' }}> | ||
| <DsTable | ||
| columns={columns} | ||
| data={tallData} | ||
| virtualized | ||
| infiniteScroll={{ | ||
| hasMore: false, | ||
| isLoadingMore: false, | ||
| onLoadMore, | ||
| thresholdPx: 200, | ||
| autoFill: false, | ||
| }} | ||
| /> | ||
| </div>, | ||
| ); | ||
|
|
||
| const container = getScrollContainer(); | ||
| container.scrollTop = container.scrollHeight; | ||
|
|
||
| await new Promise((resolve) => setTimeout(resolve, 200)); | ||
| expect(onLoadMore).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not call onLoadMore while isLoadingMore is true', async () => { | ||
| const onLoadMore = vi.fn(); | ||
| const tallData = generateTestData(200); | ||
|
|
||
| await page.render( | ||
| <div style={{ height: '400px' }}> | ||
| <DsTable | ||
| columns={columns} | ||
| data={tallData} | ||
| virtualized | ||
| infiniteScroll={{ | ||
| hasMore: true, | ||
| isLoadingMore: true, | ||
| onLoadMore, | ||
| thresholdPx: 200, | ||
| autoFill: false, | ||
| }} | ||
| /> | ||
| </div>, | ||
| ); | ||
|
|
||
| const container = getScrollContainer(); | ||
| container.scrollTop = container.scrollHeight; | ||
|
|
||
| await new Promise((resolve) => setTimeout(resolve, 200)); | ||
| expect(onLoadMore).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('autoFill triggers onLoadMore on mount when content does not fill the viewport', async () => { | ||
| const onLoadMore = vi.fn(); | ||
| const shortData = generateTestData(3); | ||
|
|
||
| await page.render( | ||
| <div style={{ height: '800px' }}> | ||
| <DsTable | ||
| columns={columns} | ||
| data={shortData} | ||
| virtualized | ||
| infiniteScroll={{ | ||
| hasMore: true, | ||
| isLoadingMore: false, | ||
| onLoadMore, | ||
| autoFill: true, | ||
| }} | ||
| /> | ||
| </div>, | ||
| ); | ||
|
|
||
| await expect.poll(() => onLoadMore.mock.calls.length, { timeout: 2000 }).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it('autoFill=false does NOT trigger onLoadMore on mount when content is short', async () => { | ||
| const onLoadMore = vi.fn(); | ||
| const shortData = generateTestData(3); | ||
|
|
||
| await page.render( | ||
| <div style={{ height: '800px' }}> | ||
| <DsTable | ||
| columns={columns} | ||
| data={shortData} | ||
| virtualized | ||
| infiniteScroll={{ | ||
| hasMore: true, | ||
| isLoadingMore: false, | ||
| onLoadMore, | ||
| autoFill: false, | ||
| }} | ||
| /> | ||
| </div>, | ||
| ); | ||
|
|
||
| await new Promise((resolve) => setTimeout(resolve, 200)); | ||
| expect(onLoadMore).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
...ystem/src/components/ds-table/components/ds-table-body-virtualized/use-infinite-scroll.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { useEffect, type RefObject } from 'react'; | ||
| import type { InfiniteScrollConfig } from './ds-table-body-virtualized.types'; | ||
|
|
||
| const DEFAULT_THRESHOLD_PX = 500; | ||
| const DEFAULT_AUTO_FILL = true; | ||
|
|
||
| interface UseInfiniteScrollResult { | ||
| /** | ||
| * Evaluates the current scroll position and, if appropriate, calls | ||
| * `onLoadMore`. Safe to call from scroll/`onChange` handlers and effects. | ||
| */ | ||
| loadDataIfNeeded: () => void; | ||
| } | ||
|
|
||
| /** | ||
| * Owns the infinite-scroll trigger logic for `DsTable`: | ||
| * threshold detection and the auto-fill loop for short initial pages. | ||
| */ | ||
| export const useInfiniteScroll = ( | ||
| scrollElementRef: RefObject<HTMLElement | null>, | ||
| rowCount: number, | ||
| config: InfiniteScrollConfig | undefined, | ||
| ): UseInfiniteScrollResult => { | ||
| const hasMore = config?.hasMore ?? false; | ||
| const isLoadingMore = config?.isLoadingMore ?? false; | ||
| const thresholdPx = config?.thresholdPx ?? DEFAULT_THRESHOLD_PX; | ||
| const autoFill = config?.autoFill ?? DEFAULT_AUTO_FILL; | ||
|
|
||
| const loadDataIfNeeded = () => { | ||
| const el = scrollElementRef.current; | ||
|
|
||
| if (!config || !hasMore || isLoadingMore || !el) { | ||
| return; | ||
| } | ||
|
|
||
| const isScrollable = el.scrollHeight > el.clientHeight; | ||
| const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight; | ||
| const isNearBottom = isScrollable && distanceToBottom <= thresholdPx; | ||
| const isNotScrollable = autoFill && !isScrollable; | ||
|
|
||
| if (isNearBottom || isNotScrollable) { | ||
| void config.onLoadMore(); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| loadDataIfNeeded(); | ||
| // loadDataIfNeeded reads the latest config values from closure; deps cover | ||
| // the meaningful inputs (data growth, hasMore, loading state). | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [rowCount, hasMore, isLoadingMore]); | ||
|
|
||
| return { loadDataIfNeeded }; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.