feat: Dropdown multi-select with filter chips, filterable support, Select All, and standalone MultiSelect component#3750
feat: Dropdown multi-select with filter chips, filterable support, Select All, and standalone MultiSelect component#3750
Conversation
…boxes - Remove @internal from multiselect prop and expose it publicly - Add _selectedValues reactive property for tracking active multi-selections - Add _inputCursor computed property for cleaner cursor style logic - Fix onSelect() to toggle items in multiselect mode and keep menu open - Fix setDisplayedValue() to show label (1 item) or "N selected" (2+ items) - Fix reset() to clear all values and dispatch values: [] in multiselect mode - Update clear button to appear when _selectedValues.length > 0 - Update keyboard handlers (Enter) to keep menu open in multiselect mode - Add checkbox visual indicator (dropdown-item-checkbox span) per list item - Set aria-multiselectable on the listbox ul in multiselect mode - Fix aria-selected and class:selected to use _selectedValues for multiselect - Use --goa-checkbox-* design tokens for checkbox indicator styling - Update Angular _onChange to emit values[] via fcChange in multiselect mode - Add multiselect to DropdownWrapper.test.svelte and 6 new tests - Add feat3596.tsx playground page with 3 test cases - Wire playground into main.tsx and app.tsx nav Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/fcd04638-6342-4f47-82d8-dbb7d5c23f5d Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
Selected items in multiselect mode now have no fill; the checkbox alone indicates selection. Hover/highlight still shows the standard hover token, keeping the interactive feel without obscuring the checkbox. Closes #3596 Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/6f1ac1cf-0c83-4d38-afcc-aba08e73bf6e Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
… dropdown Adds a `showchips` prop (default false). When enabled on a multiselect dropdown, a row of removable `goa-filter-chip` elements is rendered below the input — one per selected value. Clicking the close icon on a chip deselects that value and dispatches a `_change` event with the updated `values` array. - Svelte: new `showchips` prop + `onChipRemove` function + template + CSS - React wrapper: `showChips` boolean prop - Angular wrapper: `showChips` @input - Unit tests: render/hide/deselect via chip removal Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/c0972c95-92d7-49f1-b5b5-fca08eb82fe4 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
|
@copilot I looked at the latest changes on the playgrond page,
|
…ected values Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/74171354-0e09-452d-8348-cbc696098aa1 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
The playground page wasn't using the |
There was a problem hiding this comment.
Pull request overview
This PR adds optional “selected value chips” to the multiselect variant of the Dropdown component across the web component, React wrapper, and Angular wrapper, plus a React playground route to demo the behavior.
Changes:
- Expose multiselect behavior on the web component and add a
showchips/showChipsoption to render removablegoa-filter-chipelements for selected values. - Update change-event handling and add unit tests for multiselect selection, clear-all behavior, and chip rendering/removal.
- Wire the new prop through React/Angular wrappers and add a playground feature route (
/features/3596) demonstrating the feature.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| libs/web-components/src/components/dropdown/DropdownWrapper.test.svelte | Test wrapper updated to pass through multiselect and showchips and to handle _change details containing either value or values. |
| libs/web-components/src/components/dropdown/Dropdown.svelte | Implements multiselect selection toggling, optional chip rendering/removal, and associated UI/ARIA updates. |
| libs/web-components/src/components/dropdown/Dropdown.spec.ts | Adds multiselect + chips unit tests (ARIA, selection toggling, event details, clear-all, chip behaviors). |
| libs/react-components/src/lib/dropdown/dropdown.tsx | Adds showChips prop and forwards it to the web component as showchips. |
| libs/angular-components/src/lib/components/dropdown/dropdown.ts | Adds showChips input/attribute forwarding and updates _change handling for multiselect values. |
| apps/prs/react/src/routes/features/feat3596.tsx | New playground page demonstrating multiselect with and without showChips. |
| apps/prs/react/src/main.tsx | Registers the new /features/3596 route. |
| apps/prs/react/src/app/app.tsx | Adds a side menu entry to navigate to the new feature route. |
| } | ||
|
|
||
| // Sync input display value when multiselect selections change externally | ||
| $: if (_multiselect && _inputEl) { |
There was a problem hiding this comment.
The reactive statement meant to keep the displayed input value in sync for multiselect doesn’t actually re-run when selections change, because it only depends on _multiselect and _inputEl (not value, _values, or _selectedValues). As a result, programmatic/external updates to value after mount can leave the input summary stale. Make this reactive statement depend on _values/_selectedValues (or value) so setDisplayedValue() runs whenever the multiselect selection changes.
| $: if (_multiselect && _inputEl) { | |
| $: if (_multiselect && _inputEl && _selectedValues) { |
| function onChipRemove(chipValue: string) { | ||
| const newValues = _selectedValues.filter((v) => v !== chipValue); | ||
| _isDirty = true; | ||
| value = newValues.length > 0 ? JSON.stringify(newValues) : ""; | ||
| dispatch(_rootEl, "_change", { name, values: newValues }, { bubbles: true }); | ||
| _isDirty = false; | ||
| syncFilteredOptions(); | ||
| setDisplayedValue(); | ||
| } |
There was a problem hiding this comment.
onChipRemove updates selection and dispatches _change even when the dropdown is disabled. Since disabled controls should not allow user-initiated value changes, guard this handler with _disabled (and/or render chips in a non-interactive state when disabled).
| $: _disabled = toBoolean(disabled); | ||
| $: _multiselect = toBoolean(multiselect); | ||
| $: _native = toBoolean(native); | ||
| $: _filterable = toBoolean(filterable) && !_native; | ||
|
|
||
| // Tracks the filtered (non-empty) selected values for multiselect mode | ||
| $: _selectedValues = _multiselect ? _values.filter((v) => v !== "") : []; | ||
| $: _showChips = _multiselect && toBoolean(showchips); | ||
|
|
There was a problem hiding this comment.
filterable remains enabled in multiselect mode (_filterable doesn’t account for _multiselect), but the input is forced readonly when _multiselect is true. This combination can break filtering logic (e.g., _inputEl.value becomes "N selected" and syncFilteredOptions() will filter against that). Either explicitly disable filtering when multiselect is enabled (set _filterable false / ignore filterable), or allow typing/filtering in multiselect by removing the readonly restriction and adjusting display behavior.
| $: _disabled = toBoolean(disabled); | ||
| $: _multiselect = toBoolean(multiselect); | ||
| $: _native = toBoolean(native); | ||
| $: _filterable = toBoolean(filterable) && !_native; | ||
|
|
There was a problem hiding this comment.
native + multiselect currently produces an unsupported/broken state (native template renders a single-select <select> and selection logic assumes a single value), but _multiselect is still allowed when _native is true. Consider preventing this combination (e.g., _multiselect = toBoolean(multiselect) && !_native and/or log a clear warning) or implementing true native multiselect support.
| _onChange(e: Event) { | ||
| const detail = { ...(e as CustomEvent<GoabDropdownOnChangeDetail>).detail, event: e }; | ||
| // Keep local value in sync with emitted detail | ||
| this.value = detail.value || null; | ||
| this.onChange.emit(detail); | ||
|
|
||
| this.markAsTouched(); | ||
| this.fcChange?.(detail.value || ""); | ||
| if (this.multiselect && detail.values !== undefined) { | ||
| // Keep local value in sync for multiselect | ||
| this.value = detail.values; | ||
| this.fcChange?.(detail.values); | ||
| } else { | ||
| // Keep local value in sync with emitted detail | ||
| this.value = detail.value || null; | ||
| this.fcChange?.(detail.value || ""); | ||
| } |
There was a problem hiding this comment.
In multiselect mode, _onChange assigns this.value = detail.values (an array). The template binds [value]="value" to the web component, but the web component expects a string (JSON) and will mis-parse an array (it ends up as a single value like "red,blue"). This will also break Angular forms round-tripping. Keep value as the JSON string for the web component (or bind a stringified value), and override writeValue/convertValueToString so string[] values are JSON.stringify’d when sent to the web component.
There was a problem hiding this comment.
Fixed in commit efecefd. The Angular _onChange now stores JSON.stringify(detail.values) for this.value (so [value]="value" passes a JSON string to the web component), while fcChange?.(detail.values) still passes the raw array to Angular forms. Added convertValueToString and writeValue overrides to handle array values written back by Angular forms (e.g., setValue(['red', 'blue'])). The showChips prop has been removed — chips always show in multiselect mode.
|
@copilot apply changes based on the comments in this thread Also, remove the option to show the selected items in text. Let's only show them as filter chips. |
…ct, fix Angular array value handling Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/f6a23f60-674e-47d0-9fdd-a2bdb4d46322 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
…d writeValue logic Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/f6a23f60-674e-47d0-9fdd-a2bdb4d46322 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
Done in commit
|
|
@copilot Does this work with the filterable property? Can I filter a list of options? If so, add it to the examples to demo. If not, add the functionality. |
…s show selections Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/506c4da1-bb99-46d6-9c11-56c88f5b1cf1 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
…all-options assertions Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/506c4da1-bb99-46d6-9c11-56c88f5b1cf1 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
Filterable multiselect now works — added in commit |
|
@copilot Add a "Select All" to the top of the multi-select filter list.
|
Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/216dc454-4203-4662-a185-578d818b6613 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
… selection in tests Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/216dc454-4203-4662-a185-578d818b6613 Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
Done in commit
Four new unit tests cover all these behaviors, and the playground Test 1 & 2 descriptions have been updated to mention Select All. |
|
@copilot Can we use the |
It's technically possible but not recommended. Here are the three blocking reasons:
Staying with the current lightweight |
Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/d27a1e9b-3dbd-44cf-80a8-d79f61039a5c Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
|
@copilot Let's add a second version to this PR: a completely new component for multi-select. Make a new Svelte component that has the same multi-select behaviour as the |
…h React and Angular wrappers Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/7dabe415-4b8a-43c3-9201-87178e549f6c Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
…red value prop Agent-Logs-Url: https://github.com/GovAlta/ui-components/sessions/7dabe415-4b8a-43c3-9201-87178e549f6c Co-authored-by: bdfranck <1479091+bdfranck@users.noreply.github.com>
Done in commit New files:
The new component has all the same multiselect behaviour as the dropdown variant:
The playground at |

Before (the change)
goa-dropdownAfter (the change)
background-size: 0.625rem→0.875rem) in multiselect dropdown items to match the standalone checkbox componentmultiselectmode: selected values always render as removable filter chips below the input; filterable + multiselect supported simultaneously; "Select All" at top of list (hidden while filtering);native + multiselectunsupported state prevented; Angular array value round-tripping fixed viaJSON.stringifygoa-multi-select/GoabMultiSelectcomponent added as a second option for developers — same multiselect behaviour (filter chips, Select All, filterable mode) but as a dedicated component usinggoa-multi-select-option/GoabMultiSelectItemchild elements/features/3596updated with an "Option B" section demonstrating the new standalone componentMake sure that you've checked the boxes below before you submit the PR
Steps needed to test
npm run serve:prs:react/features/3596GoabMultiSelect+GoabMultiSelectItem