feat: #ENABLING-968 virtualize long lists in Table and Dropdown (TanStack Virtual) - #522
Conversation
There was a problem hiding this comment.
Pull request overview
Adds opt-in virtualization for long lists in @edifice.io/react’s Table and Dropdown using TanStack Virtual, to keep DOM size stable and avoid UI freezes on large datasets, with accompanying Storybook examples, tests, and styling updates.
Changes:
- Add data-driven Table API (
items/renderRow/header) with automatic virtualization past a threshold whenmaxHeightis set. - Introduce
Dropdown.VirtualizedMenu(data-driven) with keyboard navigation viaaria-activedescendantand optional integrated search. - Add TanStack Virtual dependency, tests, and Storybook stories; adjust table striping for spacer rows.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Adds @tanstack/react-virtual to the workspace catalog. |
| packages/react/package.json | Consumes @tanstack/react-virtual via catalog:. |
| packages/react/src/components/Table/components/Table.tsx | Implements data-driven Table rendering + virtualization switch. |
| packages/react/src/components/Table/components/TableVirtualizedBody.tsx | Adds virtualized <tbody> with dynamic row measurement + spacer rows. |
| packages/bootstrap/src/components/_table.scss | Adds .table--virtualized striping based on data-parity and neutralizes spacer rows. |
| packages/react/src/components/Table/Table.stories.tsx | Adds Storybook story for large virtualized Table list. |
| packages/react/src/components/Table/Table.spec.tsx | Adds tests for legacy Table API and virtualization behavior. |
| packages/react/src/components/Dropdown/DropdownVirtualizedMenu.tsx | Adds new virtualized dropdown menu component with search + keyboard nav. |
| packages/react/src/components/Dropdown/DropdownVirtualizedMenu.spec.tsx | Adds tests for virtualization, keyboard navigation, and searchable mode. |
| packages/react/src/components/Dropdown/Dropdown.tsx | Exposes Dropdown.VirtualizedMenu on the compound API. |
| packages/react/src/components/Dropdown/index.ts | Re-exports the virtualized menu module. |
| packages/react/src/components/Dropdown/stories/Dropdown.stories.tsx | Adds Storybook stories for virtualized searchable select + multi-select patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| overflowY: 'auto', | ||
| }; | ||
|
|
||
| const isDataDriven = !!items && !!renderRow; |
| <tr aria-hidden data-virtual-spacer style={{ height: paddingTop }}> | ||
| <td style={{ height: paddingTop, padding: 0, border: 0 }} /> | ||
| </tr> |
| <tr aria-hidden data-virtual-spacer style={{ height: paddingBottom }}> | ||
| <td style={{ height: paddingBottom, padding: 0, border: 0 }} /> | ||
| </tr> |
| useEffect(() => { | ||
| setActiveIndex((index) => | ||
| filteredItems.length === 0 | ||
| ? 0 | ||
| : Math.min(index, filteredItems.length - 1), | ||
| ); | ||
| }, [filteredItems.length]); |
| const handleNavKeyDown = ( | ||
| event: KeyboardEvent<HTMLInputElement | HTMLDivElement>, | ||
| ) => { | ||
| const lastIndex = filteredItems.length - 1; | ||
| switch (event.key) { | ||
| case 'ArrowDown': | ||
| event.preventDefault(); | ||
| setActiveIndex((index) => Math.min(index + 1, lastIndex)); | ||
| break; | ||
| case 'ArrowUp': | ||
| event.preventDefault(); | ||
| setActiveIndex((index) => Math.max(index - 1, 0)); | ||
| break; | ||
| case 'Home': | ||
| event.preventDefault(); | ||
| setActiveIndex(0); | ||
| break; | ||
| case 'End': | ||
| event.preventDefault(); | ||
| setActiveIndex(lastIndex); | ||
| break; |
| id={optionId(index)} | ||
| role="option" | ||
| aria-selected={active} | ||
| data-index={index} |
damienromito
left a comment
There was a problem hiding this comment.
Je trouve que le nom VirtualizedMenu n'est pas tres explicite quand on ne connait pas react-virtual mais, Claude m'a calmé en disant que si storybook explique ça le nom devient acceptable ;)
ab73b57 to
434033a
Compare
435d1b5 to
b07e5e7
Compare
b07e5e7 to
135a73b
Compare
|
Le composant est fonctionnel et le code me semble bon :) |
135a73b to
97f1785
Compare
9333533 to
4b64efa
Compare
2e5e154 to
b44e6a8
Compare
2b0b45c to
590c081
Compare
Add opt-in, data-driven virtualization to Table (Approach A), keeping the existing compound API untouched (non-breaking). - Add @tanstack/react-virtual dependency (catalog + packages/react) - Table: new items/renderRow/header API; above virtualizeThreshold (and with maxHeight set) only the visible rows (+ overscan) are mounted, with top/bottom spacer rows and dynamic row measurement (variable heights) - Pass the scroll element via callback-ref state so the virtualizer measures the container reliably on mount (avoids the empty-tbody race) - Drive zebra striping from the real item index in virtualized mode, since spacer rows would shift :nth-of-type (.table--virtualized SCSS) - Add a large-list (~6 800 rows) Storybook story and unit tests Verified live in Storybook: constant DOM node count while scrolling a ~6 800-row list, correct striping and column alignment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Dropdown.VirtualizedMenu, an opt-in data-driven menu for very long option lists, leaving the existing compound API untouched (non-breaking). - items/renderItem API; only the visible options (+ overscan) are mounted - Keyboard navigation on the listbox container via aria-activedescendant (roving) instead of focusing each option, required since off-screen options are not mounted: arrows/Home/End move + scrollToIndex, Enter selects, Escape/Tab close - role=listbox/option, aria-selected, dynamic option measurement - Pass the scroll element via callback-ref state (reliable measure on open) - Add a 5 000-option story with live filtering, and unit tests Verified live in Storybook: constant DOM node count on 5 000 options, keyboard nav scrolls the active option into view, filtered list stays virtualized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rated search Wrap the virtualized list in a dedicated block-level scroll container instead of reusing the flex .dropdown-menu element: the flex layout collapsed the sizer height, so the scrollbar only spanned the mounted window and the end of the list was unreachable. - Panel (flex, dropdown-menu styling) holds an optional search field + a block-level scrollable listbox whose sizer keeps its full height - Add searchable mode (combobox): integrated SearchBar on top, focus stays in the field, arrows/Home/End/Enter drive the listbox, built-in filtering via getItemText, onSearch callback, no-result message - Rework the story into a realistic searchable single-select over ~6 800 options, moved last, with a detailed usage description - Extend unit tests with the searchable/combobox behaviour Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n story Demonstrate the establishments-filter case: a searchable virtualized menu with a presentational Checkbox per row, consumer-owned selection (Set), onSelect toggling and closeOnSelect=false. The story description explains how to exploit the selection and why Dropdown.CheckboxItem can't be used inside a virtualized menu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b44e6a8 to
1f67a58
Compare
Description
Virtualise les listes longues des composants
TableetDropdowndu frontend-framework (via TanStack Virtual), afin d'afficher de très grandes listes sans figer l'UI — le nombre de nœuds DOM reste constant quelle que soit la volumétrie. Les apps n'ont plus à réimplémenter une solution de windowing.Changement non-breaking : les API existantes (compound
Table.*etDropdown.Menu) sont inchangées ; la virtualisation est opt-in et data-driven.Changements
Table — virtualisation auto au-delà d'un seuil
items/renderRow/headersurTable(en plus du compound existant).virtualizeThreshold(défaut 100) et avecmaxHeight, seules les lignes visibles (+ overscan) sont montées ; lignes spacer haut/bas, mesure dynamique des hauteurs (hauteurs variables supportées)..table--virtualized), les lignes spacer ne décalant plus:nth-of-type.Dropdown — nouveau
Dropdown.VirtualizedMenu(opt-in)items/renderItem, seules les options visibles sont montées.aria-activedescendant(roving, sans.focus()par item — requis car les options hors écran ne sont pas montées) : flèches / Home / End +scrollToIndex, Enter sélectionne, Escape / Tab ferme.searchable, pattern combobox) : champ en haut du panneau, focus dans le champ, filtrage interne viagetItemText, message « pas de résultat », callbackonSearch.renderItem(Checkbox présentationnelle) +onSelect+closeOnSelect={false}(cas filtre « Établissements » d'Assistance ENT).display:blockdédié (ledisplay:flexde.dropdown-menuécrasait sinon la hauteur du sizer → scrollbar incohérente).Dépendance : ajout de
@tanstack/react-virtual(catalog +@edifice.io/react, auto-externalisée au build).Which Package changed?
Has the documentation changed?
Type of change
Comment tester
pnpm --filter @edifice.io/react test→ suite verte (tests Table + Dropdown virtualisés).pnpm docs:TableetDropdownexistantes (API compound inchangée).Checklist: