feat: DH-19683: Multi-select combobox component#2685
Conversation
mofojed
left a comment
There was a problem hiding this comment.
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[]; |
There was a problem hiding this comment.
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.
| /** | ||
| * TODO: this is pretty fragile | ||
| */ | ||
| function cleanReactKey(rawKey: string): string { | ||
| return rawKey.replace(/^\.\$/, ''); | ||
| } |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| if (isItemElement(child)) { | ||
| const item = itemElementToFlat(child as ItemElement<unknown>); |
There was a problem hiding this comment.
No need to cast
| const item = itemElementToFlat(child as ItemElement<unknown>); | |
| const item = itemElementToFlat(child); |
| const sectionItems: MultiSelectFlatItem[] = []; | ||
| ensureArray(section.props.children).forEach(sectionChild => { | ||
| if (isItemElement(sectionChild)) { | ||
| const item = itemElementToFlat(sectionChild as ItemElement<unknown>); |
There was a problem hiding this comment.
| const item = itemElementToFlat(sectionChild as ItemElement<unknown>); | |
| const item = itemElementToFlat(sectionChild); |
| /** | ||
| * 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, ''); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Improved comment
There was a problem hiding this comment.
We updated the package.json, you need to run npm install to update the package-lock.json as well.
There was a problem hiding this comment.
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/MultiSelectNormalizedcomponents (and supporting hooks/utilities/styles) to@deephaven/components. - Added
useMultiPickerProps+MultiSelectwrapper to@deephaven/jsapi-componentsfor 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.
| <MultiSelectNormalized | ||
| // eslint-disable-next-line react/jsx-props-no-spreading | ||
| {...restPickerProps} | ||
| onInputChange={onInputChange} | ||
| onOpenChange={onOpenChange} |
| // 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 ( |
| * `<listId>-option-<normalizedKey>` matches the actual rendered DOM `id`. | ||
| */ | ||
| function normalizeKey(key: string): string { | ||
| return key.replace(/\s*/g, ''); |
| const rangeSet = dh.RangeSet.ofItems(rowIndices); | ||
| const tableData = await tableCopy.createSnapshot({ | ||
| rows: rangeSet, | ||
| columns: tableCopy.columns, | ||
| }); |
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
mofojed
left a comment
There was a problem hiding this comment.
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
|
Looks like Spectrum recommends manually filtering if you want something more advanced |
|
Fixed the searching issue |
|
Don't merge yet, seems to be some sizing issues currently |
Add multi-select combobox component. Tested with (and required for) deephaven/deephaven-plugins#1349