Skip to content

feat: DH-19683: Multi-select combobox component#2685

Open
jnumainville wants to merge 20 commits into
deephaven:mainfrom
jnumainville:DH-19683-multi-select-component-in-deephaven-ui
Open

feat: DH-19683: Multi-select combobox component#2685
jnumainville wants to merge 20 commits into
deephaven:mainfrom
jnumainville:DH-19683-multi-select-component-in-deephaven-ui

Conversation

@jnumainville
Copy link
Copy Markdown
Contributor

@jnumainville jnumainville commented May 19, 2026

Add multi-select combobox component. Tested with (and required for) deephaven/deephaven-plugins#1349

Copy link
Copy Markdown
Member

@mofojed mofojed left a comment

Choose a reason for hiding this comment

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

We also need to add this to the StyleGuide, likely in the same spot that the ComboBox is shown in the style guide as well.

/** Spectrum `LoadingState` for the items collection. */
loadingState: LoadingState | undefined;
/** JSX children to render inside `<ListBox>`. */
filteredJsxChildren: ReactElement[];
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.

nit: I don't like this name filteredJsxChildren, should probably just be children or shownElements or something.
Though this component probably isn't really used outside of multi select, so it doesn't really matter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed

Comment on lines +35 to +40
/**
* TODO: this is pretty fragile
*/
function cleanReactKey(rawKey: string): string {
return rawKey.replace(/^\.\$/, '');
}
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.

TODO
Why is this even necessary, what's going on here? Where is this regex coming from? I don't see it in the react-spectrum repo either.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added comment

}

if (isItemElement(child)) {
const item = itemElementToFlat(child as ItemElement<unknown>);
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.

No need to cast

Suggested change
const item = itemElementToFlat(child as ItemElement<unknown>);
const item = itemElementToFlat(child);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

const sectionItems: MultiSelectFlatItem[] = [];
ensureArray(section.props.children).forEach(sectionChild => {
if (isItemElement(sectionChild)) {
const item = itemElementToFlat(sectionChild as ItemElement<unknown>);
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.

Suggested change
const item = itemElementToFlat(sectionChild as ItemElement<unknown>);
const item = itemElementToFlat(sectionChild);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

Comment on lines +38 to +46
/**
* TODO: fragile, see if there is a better way to link focused state
* Replicates the key-normalization Spectrum applies to listbox option ids
* (see `@react-aria/listbox/src/utils.ts`). Whitespace is stripped so that
* `<listId>-option-<normalizedKey>` matches the actual rendered DOM `id`.
*/
function normalizeKey(key: string): string {
return key.replace(/\s*/g, '');
}
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.

Same TODO
Though we can just say we're pulling it from listbox utils and it should match: https://github.com/adobe/react-spectrum/blob/9508b15bf8c0e968c56220548207cc57c7e4f57c/packages/react-aria/src/listbox/utils.ts#L31
I don't know where the other regex came from though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Improved 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.

We updated the package.json, you need to run npm install to update the package-lock.json as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new table-backed multi-select combobox for @deephaven/jsapi-components, backed by a new MultiSelect implementation in @deephaven/components (including filtering, keyboard navigation, scroll handling, and selection normalization). This enables multi-selection with server-side (table) search while preserving selected items even when the current filter excludes them.

Changes:

  • Added MultiSelect / MultiSelectNormalized components (and supporting hooks/utilities/styles) to @deephaven/components.
  • Added useMultiPickerProps + MultiSelect wrapper to @deephaven/jsapi-components for DH table integration, including snapshotting selected rows for label resolution.
  • Updated selection stringification logic to preserve selected keys that are not present in the current (filtered) item list.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/jsapi-components/src/spectrum/utils/useMultiPickerProps.ts Adds table-backed multi-picker props hook with snapshot-based label resolution + label caching.
packages/jsapi-components/src/spectrum/utils/useMultiPickerProps.test.ts Adds unit tests for snapshotting behavior in useMultiPickerProps.
packages/jsapi-components/src/spectrum/utils/index.ts Exports useMultiPickerProps.
packages/jsapi-components/src/spectrum/MultiSelect.tsx Adds jsapi-components wrapper around components’ MultiSelectNormalized, integrating DH search behavior.
packages/jsapi-components/src/spectrum/MultiSelect.test.tsx Adds tests for wrapper search-text/open behavior.
packages/jsapi-components/src/spectrum/MultiPickerProps.ts Introduces table-backed prop type for multi-picker components.
packages/jsapi-components/src/spectrum/index.ts Exports new MultiSelect and MultiPickerProps.
packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts Preserves selected keys that are filtered out of the current item list.
packages/components/src/spectrum/multiSelect/useMultiSelectState.ts Adds selection state management for multi-select (controlled/uncontrolled + filtered preservation).
packages/components/src/spectrum/multiSelect/useMultiSelectState.test.ts Tests selection state behavior.
packages/components/src/spectrum/multiSelect/useMultiSelectScrollListener.ts Adds popover scroll listener attachment logic.
packages/components/src/spectrum/multiSelect/useMultiSelectScrollListener.test.ts Tests scroll listener attachment/detachment behavior.
packages/components/src/spectrum/multiSelect/useMultiSelectNormalizedProps.tsx Adds normalized-items adapter (sections + key stringification + renderer wiring).
packages/components/src/spectrum/multiSelect/useMultiSelectLoadingSpinner.ts Adds debounced spinner visibility logic during loading/filtering.
packages/components/src/spectrum/multiSelect/useMultiSelectLoadingSpinner.test.ts Tests spinner debounce/visibility rules.
packages/components/src/spectrum/multiSelect/useMultiSelectKeyboard.ts Adds keyboard handling + virtual focus + aria-activedescendant syncing.
packages/components/src/spectrum/multiSelect/useMultiSelectKeyboard.test.ts Tests keyboard behavior for open/close/toggle/removal basics.
packages/components/src/spectrum/multiSelect/useMultiSelectFilter.ts Adds controlled/uncontrolled input + client-side filter with server-side filter bypass option.
packages/components/src/spectrum/multiSelect/useMultiSelectFilter.test.ts Tests filter logic and server-side bypass behavior.
packages/components/src/spectrum/multiSelect/multiSelectUtils.tsx Adds JSX flattening + filtering + selection resolution utilities for multi-select internals.
packages/components/src/spectrum/multiSelect/multiSelectUtils.test.tsx Tests multi-select utilities (flatten/filter/resolve).
packages/components/src/spectrum/multiSelect/MultiSelectTag.tsx Adds selected-item tag UI with remove affordance.
packages/components/src/spectrum/multiSelect/MultiSelectTag.test.tsx Tests tag rendering and remove callback.
packages/components/src/spectrum/multiSelect/MultiSelectProps.ts Adds public prop surface for MultiSelect + normalized variant props.
packages/components/src/spectrum/multiSelect/MultiSelectNormalized.tsx Adds normalized variant wrapper around MultiSelect.
packages/components/src/spectrum/multiSelect/MultiSelectListBox.tsx Adds listbox popover content component with empty-state handling.
packages/components/src/spectrum/multiSelect/MultiSelect.tsx Adds main MultiSelect composite implementation (UI + overlay + form integration).
packages/components/src/spectrum/multiSelect/MultiSelect.scss Adds Spectrum-aligned styling for MultiSelect, tags, focus highlight, etc.
packages/components/src/spectrum/multiSelect/index.ts Exports multiSelect module.
packages/components/src/spectrum/index.ts Re-exports multiSelect module from Spectrum barrel.
packages/components/package.json Adds required React Aria/Spectrum dependencies for the new component.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +60
<MultiSelectNormalized
// eslint-disable-next-line react/jsx-props-no-spreading
{...restPickerProps}
onInputChange={onInputChange}
onOpenChange={onOpenChange}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +282 to +290
// Ignore null relatedTarget (DOM churn during re-render).
// Real dismisses carry a relatedTarget or are handled by Spectrum.
if (related == null) {
return;
}
if (triggerRef.current != null && triggerRef.current.contains(related)) {
return;
}
if (
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

* `<listId>-option-<normalizedKey>` matches the actual rendered DOM `id`.
*/
function normalizeKey(key: string): string {
return key.replace(/\s*/g, '');
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +185 to +189
const rangeSet = dh.RangeSet.ofItems(rowIndices);
const tableData = await tableCopy.createSnapshot({
rows: rangeSet,
columns: tableCopy.columns,
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 54.98489% with 298 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.22%. Comparing base (d76c08b) to head (50f1290).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
...omponents/src/spectrum/multiSelect/MultiSelect.tsx 1.20% 164 Missing ⚠️
...src/spectrum/multiSelect/useMultiSelectKeyboard.ts 45.00% 66 Missing ⚠️
...mponents/src/spectrum/utils/useMultiPickerProps.ts 78.99% 25 Missing ⚠️
...trum/multiSelect/useMultiSelectNormalizedProps.tsx 0.00% 17 Missing ⚠️
packages/code-studio/src/styleguide/Pickers.tsx 0.00% 13 Missing ⚠️
...ts/src/spectrum/multiSelect/MultiSelectListBox.tsx 0.00% 4 Missing ⚠️
...src/spectrum/multiSelect/MultiSelectNormalized.tsx 0.00% 3 Missing ⚠️
...ents/src/spectrum/multiSelect/multiSelectUtils.tsx 97.36% 2 Missing ⚠️
...ectrum/multiSelect/useMultiSelectLoadingSpinner.ts 93.10% 2 Missing ⚠️
...ectrum/multiSelect/useMultiSelectScrollListener.ts 95.83% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2685      +/-   ##
==========================================
+ Coverage   49.76%   50.22%   +0.46%     
==========================================
  Files         774      787      +13     
  Lines       43945    44824     +879     
  Branches    11330    11605     +275     
==========================================
+ Hits        21871    22515     +644     
- Misses      22027    22263     +236     
+ Partials       47       46       -1     
Flag Coverage Δ
unit 50.22% <54.98%> (+0.46%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jnumainville jnumainville requested a review from mofojed May 21, 2026 18:03
Copy link
Copy Markdown
Member

@mofojed mofojed left a comment

Choose a reason for hiding this comment

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

A couple of things after playing with it some more in the style guide...

  • Is there a way to do a case insensitive search? It's annoying that it is case sensitive. Though I guess this is the case with the ComboBox too...
  • When I type in a search parameter, it seems to truncate the existing selected items. I think because they're filtered out or something, but we shouldn't be changing the previously selected items at all when you're filtering for a new item
Image

@jnumainville
Copy link
Copy Markdown
Contributor Author

Looks like Spectrum recommends manually filtering if you want something more advanced
https://react-spectrum.adobe.com/v3/ComboBox.html#custom-filtering
Could be an interesting extension someday

@jnumainville
Copy link
Copy Markdown
Contributor Author

Fixed the searching issue

@jnumainville jnumainville requested a review from mofojed May 21, 2026 21:41
@jnumainville
Copy link
Copy Markdown
Contributor Author

Don't merge yet, seems to be some sizing issues currently

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