Skip to content

feat: #ENABLING-968 virtualize long lists in Table and Dropdown (TanStack Virtual) - #522

Open
pascalsaussier-edifice wants to merge 4 commits into
develop-enablingfrom
feat-ENABLING-968-virtualize-dropdown-table
Open

feat: #ENABLING-968 virtualize long lists in Table and Dropdown (TanStack Virtual)#522
pascalsaussier-edifice wants to merge 4 commits into
develop-enablingfrom
feat-ENABLING-968-virtualize-dropdown-table

Conversation

@pascalsaussier-edifice

Copy link
Copy Markdown
Collaborator

Description

Virtualise les listes longues des composants Table et Dropdown du 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.* et Dropdown.Menu) sont inchangées ; la virtualisation est opt-in et data-driven.

Changements

Table — virtualisation auto au-delà d'un seuil

  • Nouvelle API opt-in items / renderRow / header sur Table (en plus du compound existant).
  • Au-delà de virtualizeThreshold (défaut 100) et avec maxHeight, seules les lignes visibles (+ overscan) sont montées ; lignes spacer haut/bas, mesure dynamique des hauteurs (hauteurs variables supportées).
  • Zébrage piloté par l'index réel en mode virtualisé (.table--virtualized), les lignes spacer ne décalant plus :nth-of-type.

Dropdown — nouveau Dropdown.VirtualizedMenu (opt-in)

  • API data-driven items / renderItem, seules les options visibles sont montées.
  • Navigation clavier au niveau du conteneur via 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.
  • Recherche intégrée optionnelle (searchable, pattern combobox) : champ en haut du panneau, focus dans le champ, filtrage interne via getItemText, message « pas de résultat », callback onSearch.
  • Multi-sélection possible via renderItem (Checkbox présentationnelle) + onSelect + closeOnSelect={false} (cas filtre « Établissements » d'Assistance ENT).
  • Détail technique : l'élément scrollable est transmis au virtualizer via callback-ref state (mesure fiable au montage), et le listbox est un conteneur display:block dédié (le display:flex de .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?

  • Components
  • Core
  • Icons
  • Hooks

@edifice.io/react (Table, Dropdown) + @edifice.io/bootstrap (_table.scss).

Has the documentation changed?

  • Storybook

Stories grande liste (Table ~6 800 lignes) ; Dropdown : VirtualizedSearchableSelect (select recherche) et VirtualizedMultiSelect (checkboxes), avec descriptions d'usage détaillées.

Type of change

  • Chore (PATCH)
  • Doc (PATCH)
  • Bug fix (PATCH)
  • New feature (MINOR)
  • Breaking change (MAJOR)

Comment tester

  1. pnpm --filter @edifice.io/react test → suite verte (tests Table + Dropdown virtualisés).
  2. pnpm docs :
    • Components / Table → VirtualizedLargeList : scroll fluide sur ~6 800 lignes, nombre de nœuds DOM constant.
    • Components / Dropdown / Base → VirtualizedSearchableSelect : select recherche sur ~6 800 options, nav clavier + scroll, filtre.
    • → VirtualizedMultiSelect : checkboxes, sélection multiple, le menu reste ouvert.
  3. Vérifier la non-régression des stories Table et Dropdown existantes (API compound inchangée).

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 when maxHeight is set.
  • Introduce Dropdown.VirtualizedMenu (data-driven) with keyboard navigation via aria-activedescendant and 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;
Comment on lines +63 to +65
<tr aria-hidden data-virtual-spacer style={{ height: paddingTop }}>
<td style={{ height: paddingTop, padding: 0, border: 0 }} />
</tr>
Comment on lines +86 to +88
<tr aria-hidden data-virtual-spacer style={{ height: paddingBottom }}>
<td style={{ height: paddingBottom, padding: 0, border: 0 }} />
</tr>
Comment on lines +132 to +138
useEffect(() => {
setActiveIndex((index) =>
filteredItems.length === 0
? 0
: Math.min(index, filteredItems.length - 1),
);
}, [filteredItems.length]);
Comment on lines +164 to +184
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;
Comment on lines +281 to +284
id={optionId(index)}
role="option"
aria-selected={active}
data-index={index}
damienromito
damienromito previously approved these changes Jun 30, 2026

@damienromito damienromito left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ;)

@pascalsaussier-edifice
pascalsaussier-edifice force-pushed the develop-enabling branch 2 times, most recently from ab73b57 to 434033a Compare July 1, 2026 09:29
@pascalsaussier-edifice
pascalsaussier-edifice force-pushed the feat-ENABLING-968-virtualize-dropdown-table branch from 435d1b5 to b07e5e7 Compare July 1, 2026 13:23
@pascalsaussier-edifice
pascalsaussier-edifice force-pushed the feat-ENABLING-968-virtualize-dropdown-table branch from b07e5e7 to 135a73b Compare July 16, 2026 13:46
@theovgl

theovgl commented Jul 20, 2026

Copy link
Copy Markdown
Member

Le composant est fonctionnel et le code me semble bon :)
Une petite remarque/question : dans l'application Assistance on a une implémentation similaire au Virtualized Multi Select mais avec une fonctionnalité qui permet de tout sélectionner. Il est peut-être pertinent d'ajouter cette fonctionnalité directement au nouveau composant, qu'est-ce que tu en penses ?

@pascalsaussier-edifice
pascalsaussier-edifice force-pushed the feat-ENABLING-968-virtualize-dropdown-table branch from 135a73b to 97f1785 Compare July 27, 2026 15:43
@pascalsaussier-edifice
pascalsaussier-edifice force-pushed the feat-ENABLING-968-virtualize-dropdown-table branch 2 times, most recently from 2e5e154 to b44e6a8 Compare August 6, 2026 13:51
pascalsaussier-edifice and others added 4 commits August 26, 2026 17:16
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>
@pascalsaussier-edifice
pascalsaussier-edifice force-pushed the feat-ENABLING-968-virtualize-dropdown-table branch from b44e6a8 to 1f67a58 Compare August 26, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants