Skip to content

feat(compactSelect): add section virtualization support#108394

Closed
JonasBa wants to merge 8 commits intomasterfrom
jonasbadalic/compactselect-section-virtualization-support
Closed

feat(compactSelect): add section virtualization support#108394
JonasBa wants to merge 8 commits intomasterfrom
jonasbadalic/compactselect-section-virtualization-support

Conversation

@JonasBa
Copy link
Member

@JonasBa JonasBa commented Feb 17, 2026

Summary

Enable CompactSelect virtualization for lists with sections (grouped options). Previously, shouldVirtualize() unconditionally returned false when any section was present, forcing all sectioned lists to render every option in the DOM.

Key changes:

  • Section-aware virtualization in ListBox: Remove the section gate from shouldVirtualize() — it now counts total options across all sections. Add section-aware height estimation for @tanstack/react-virtual (sectionHeaderHeights, SECTION_SEPARATOR_HEIGHT). Move SectionSeparator inside SectionWrap so measureElement captures the full section height including separators
  • Fix HiddenSectionToggle for virtualized sections: Replace document.querySelector DOM queries with a React context-based ref map (SectionToggleRefContext). Section toggle buttons register/deregister on mount/unmount, so HiddenSectionToggle works regardless of whether sections are scrolled into view
  • GridList virtualization: Extract useVirtualizedItems to a shared module, wire it into GridList, GridListSection, and GridListOption. Non-virtualized path preserves identical DOM structure to avoid regressions
  • Remove projectPageFilter sizeLimit workaround: Drop sizeLimit={25} fallback now that virtualization handles rendering performance for large project lists
  • Migrate account emails form to new TanStack-based form system

Replaces Form/JsonForm/accountEmailsFields with useScrapsForm + Zod
validation. Uses useMutation + fetchMutation for the POST submission,
invalidates the email list query on success, and resets the field
after a successful add.
Remove the section gate from shouldVirtualize() so sectioned lists
can now virtualize when total option count exceeds the threshold.
Update height estimation to account for section headers and separators,
and move SectionSeparator inside SectionWrap so measureElement captures
full section height.
… refs

Replace document.querySelector in HiddenSectionToggle with a React
context-based ref map. SectionToggle registers its button element on
mount and cleans up on unmount, so HiddenSectionToggle can find visible
counterparts regardless of virtualization state.
Extract useVirtualizedItems to a shared module and wire it into
GridList. GridListSection and GridListOption now accept virtualization
props (ref, data-index). Move SectionSeparator inside SectionWrap
in GridListSection for correct height measurement. Pass virtualized
and showSectionHeaders through from List to GridList.
… changes

Use conditional mergeRefs in GridListOption to preserve the original
ref object when no external ref is provided, fixing focus management.
Render Container wrappers only when virtualized to avoid DOM changes
in the non-virtualized path. Fix separator elementType to match the
actual div element.
Remove the hardcoded sizeLimit={25} fallback now that virtualization
handles rendering performance for large project lists.
@JonasBa JonasBa requested a review from a team as a code owner February 17, 2026 22:17
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Feb 17, 2026
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

size={size}
isFirst={row.index === 0}
/>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

GridListSection ignores showSectionHeaders prop causing height mismatch

Medium Severity

GridList accepts showSectionHeaders and passes it to useVirtualizedItems for height estimation, but never passes it to GridListSection. The section component always renders headers and separators regardless. This contrasts with ListBoxSection, which receives and respects showSectionHeaders. When showSectionHeaders is false, the virtualizer estimates zero height for headers but the DOM still renders them, causing a mismatch between estimated and actual section heights that can produce visual glitches (overlapping items or gaps) with virtualization enabled.

Additional Locations (1)

Fix in Cursor Fix in Web

});
return longestChild ? getItemsWithKeys([longestChild]) : [];
}
return getItemsWithKeys([longestOption]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Width measurement may pick wrong option across sections

Medium Severity

The maxBy call compares section header label lengths against option label/textValue lengths in a single pass. If a section header has the longest text, it "wins," and then only the longest child within that specific section is picked for width measurement. Options in other sections or at the top level that are actually wider get ignored, potentially resulting in a menu that's too narrow to display them.

Fix in Cursor Fix in Web

const SectionSeparatorInner = styled('div')`
border-top: solid 1px ${p => p.theme.tokens.border.secondary};
margin: ${p => p.theme.space.xs} ${p => p.theme.space.lg};
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicate SectionSeparatorInner styled component across files

Low Severity

SectionSeparatorInner is identically defined in both gridList/section.tsx and listBox/section.tsx. There's already a shared SectionSeparator in styles.tsx (exported from the compactSelect package). This new component could be extracted to the shared styles module to avoid the duplication and risk of the two copies diverging.

Additional Locations (1)

Fix in Cursor Fix in Web

Replace height="100%" with flex="1 1 0" and minHeight="0" on the
scroll container. height: 100% didn't resolve to a constrained height
because the parent Stack has no definite height, causing the scroll
container to expand to fit all content and the virtualizer to render
all items as "visible".
Add flex="1 1 0" to the Stack wrapper in control.tsx so it fills
available space in StyledOverlay. This gives the scroll container's
height: 100% a definite parent height to resolve against, enabling
the virtualizer to correctly determine which items are visible.
@JonasBa
Copy link
Member Author

JonasBa commented Feb 18, 2026

Tried taking riptide for a spin, and the implemented solution doesn't actually work.

@JonasBa JonasBa closed this Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant