Skip to content

fix(Dropdown): keep selected value inside input for searchable single select#3405

Open
rivka-ungar wants to merge 14 commits into
masterfrom
fix/single-searchable-dropdown-a11y-selected-value
Open

fix(Dropdown): keep selected value inside input for searchable single select#3405
rivka-ungar wants to merge 14 commits into
masterfrom
fix/single-searchable-dropdown-a11y-selected-value

Conversation

@rivka-ungar

@rivka-ungar rivka-ungar commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Problem

The searchable single-select combobox forced the input value to null after every selection and rendered the selected label as a visual overlay on top of an empty input.

Screen readers read what's inside the input — not the overlay. So a combobox with an active selection announced as "Edit combo, collapsed, blank" (JAWS). The user had no way to know what they'd selected.

This fails WCAG 2.1 SC 4.1.2 — Name, Role, Value (Level A), which requires the current value of a form control to be programmatically determinable.

Fix

Stop overriding Downshift's default so the selected item's label lives inside the input, where assistive technologies can read it. The selected value is no longer a separate visual layer.

Supporting changes keep the component's existing behavior intact:

  • initialInputValue seeded with the selected label, so a defaultValue/controlled value is visible on mount (previously the overlay handled this).
  • onInputValueChange filters the list only on real user typing (InputChange), ignoring the label Downshift writes into the input on selection/blur.
  • onIsOpenChange resets the filter when the menu closes, so reopening shows the full option list — matching the component's prior behavior and the documented combobox pattern (same approach Chakra v3 / Ark UI recommend: reset the filter on open).

The overlay in SingleSelectTrigger now renders only for non-searchable single select (where inputValue stays null), so that path is unaffected. Multi-select uses a separate hook and is untouched.

Behavior parity

Aligned with Chakra v3 (Ark UI / Zag.js):

Behavior Chakra v3 This PR
Selected label in input ✅ default
aria-selected + activedescendant on selected option at open ✅ (already)
Reopen shows all options ✅ (recommended reset())

Storybook

Adds a dedicated Components/Dropdown/Searchable single select page documenting all searchable single-select variants: overview, sizes, states, default/controlled value, icons & avatars, groups (sticky titles & dividers), tooltips, clearable / max-height, and custom filter / empty message. The default-value story demonstrates the selected value living inside the input.

Tests

  • All 71 Dropdown tests pass.
  • Rewrote 2 tests that asserted the old overlay behavior:
    • "faded selected item when focused" → now asserts the input holds the selected value.
    • "indent startElement not in selected value" → moved to searchable: false, since the indent-stripping overlay logic now applies only to non-searchable single select.

Test plan

  • Searchable single select: select an option → label shows in the input, no overlay; tab away and back → screen reader announces the selected value
  • Reopen after selecting → full option list shown, selected option marked/highlighted
  • Typing filters the list; clearing shows all
  • defaultValue / controlled value shows the label in the input on mount
  • Non-searchable single select and multi-select unchanged

🤖 Generated with Claude Code

… select

The searchable single-select combobox previously forced the input value to
null after selection and rendered the selected label as a visual overlay on
top of an empty input. Screen readers read the input, not the overlay, so a
selected combobox announced as "blank" — failing WCAG 2.1 SC 4.1.2 (Name,
Role, Value, Level A).

Stop overriding Downshift's default so the selected item's label lives inside
the input and is exposed to assistive technologies. Supporting changes keep
the component's existing behavior intact:

- Seed initialInputValue with the selected label so a defaultValue/value is
  visible on mount (previously handled by the overlay).
- Filter the list only on real user typing (InputChange); ignore the label
  Downshift writes into the input on selection/blur.
- Reset the filter when the menu closes so reopening shows the full list,
  matching the prior behavior and the documented combobox pattern.

The overlay in SingleSelectTrigger now naturally renders only for
non-searchable single select (where inputValue stays null), so that path is
unaffected. Multi-select uses a separate hook and is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rivka-ungar rivka-ungar requested a review from a team as a code owner June 10, 2026 18:33
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (11) 📘 Rule violations (1)

Grey Divider


Action required

1. Stale textInput summary 🐞 Bug ≡ Correctness ⭐ New
Description
In multi-select textInput mode, the input’s displayed summary is only seeded via
initialInputValue and updated on internal Downshift events; when value is controlled and the
parent changes value externally, there is no synchronization path to update Downshift’s
inputValue, so the input can display a stale summary that no longer matches the actual selection.
Code

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[R87-88]

+    initialInputValue: inputValueProp ?? (textInput ? currentSelectedItems.map(i => i.label).join(", ") : ""),
    id,
Evidence
The hook supports controlled selection via value?: Item[], but the textInput summary is only
applied through initialInputValue (initialization-only) and reducer branches tied to internal
events; no code updates Downshift’s inputValue when value changes externally, and the top-level
Dropdown does not remount controllers on value changes.

packages/core/src/components/Dropdown/Dropdown.types.ts[10-55]
packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[28-92]
packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[140-182]
packages/core/src/components/Dropdown/Dropdown.tsx[14-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
For multi-select `textInput`, the input shows a comma-separated summary derived from selected items. But `useCombobox` is only initialized with that summary via `initialInputValue`, and there is no effect or controlled `inputValue` prop to keep it in sync when `value` (controlled selected items) changes outside of Downshift interactions.

### Issue Context
This causes UI divergence in common flows like form reset, server-driven updates, or parent state changes: `selectedItems` in context updates (because it uses `value`), but the input continues to show the old summary.

### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[28-118]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[140-182]

### Suggested fix
Implement synchronization in `textInput` mode, for example:
- Maintain a dedicated `displayInputValue` state in the hook and pass it to `useCombobox` as `inputValue` (controlled), updating it on:
 - user typing (InputChange)
 - selection changes (when `currentSelectedItems` changes)
 - close/blur (restore summary)
- Or, if available in your Downshift hook return, call `setInputValue(summary)` in a `useEffect` when `textInput` is true and `currentSelectedItems` changes, guarded so it doesn’t overwrite active user typing (e.g., only when menu is closed / input not focused).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unused hook parameter 🐞 Bug ⚙ Maintainability
Description
useDropdownMultiCombobox adds an interactiveChips parameter but never uses it, which violates the
core package’s TypeScript unused-vars ESLint rule and can fail CI/linting. This also indicates the
hook signature doesn’t match its implemented behavior.
Code

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[R23-26]

+  id?: string,
+  onOptionRemove?: (option: T) => void,
+  textInput?: boolean,
+  interactiveChips?: boolean
Evidence
The hook signature includes interactiveChips but the implementation’s control flow only uses
textInput (e.g., enableToggle = textInput), leaving interactiveChips unused. Core ESLint
config marks unused variables/args as an error for TS/TSX files, so this is a merge-blocking lint
issue when lint runs.

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[7-36]
packages/core/.eslintrc.cjs[97-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useDropdownMultiCombobox` declares `interactiveChips?: boolean` but never references it. In `packages/core`, `@typescript-eslint/no-unused-vars` is configured as an error for TS/TSX files, so this can break lint/CI.
### Issue Context
`interactiveChips` is already consumed in UI (Trigger) and context; it does not appear to be needed inside this hook. The cleanest fix is to remove the argument from the hook signature and from the call site(s). If you want to keep it for future work, rename it to `_interactiveChips` to satisfy the lint rule.
### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[7-36]
- packages/core/src/components/Dropdown/modes/DropdownMultiComboboxController.tsx[47-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Chip focus fails without overflow 🐞 Bug ≡ Correctness
Description
In interactiveChips mode, ArrowLeft from the input only focuses the overflow (+N) badge; when
there is no overflow badge (all chips visible), ArrowLeft does nothing so keyboard users can’t reach
chips via the documented navigation. This contradicts the interactiveChips prop contract that
ArrowLeft/Backspace from the input moves focus to the last chip.
Code

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[R46-57]

+        <div
+          className={styles.multiWrapper}
+          onKeyDown={e => {
+            if (
+              e.key === "ArrowLeft" &&
+              e.target instanceof HTMLInputElement &&
+              !e.target.value &&
+              overflowBadgeRef.current
+            ) {
+              overflowBadgeRef.current.focus();
+            }
+          }}
Evidence
The code explicitly gates focus transfer on the presence of overflowBadgeRef.current, and the only
other ArrowLeft focus transfer logic lives on the overflow-counter wrapper that is rendered only
when hiddenCount > 0. The prop’s own documentation states ArrowLeft/Backspace should move focus to
the last chip, which isn’t satisfied when there’s no overflow.

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-57]
packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-173]
packages/core/src/components/Dropdown/Dropdown.types.ts[29-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`interactiveChips` documents that ArrowLeft/Backspace from the input moves focus to the last chip, but the current implementation only moves focus to the overflow badge when it exists. When `hiddenCount===0` (no overflow), there is no focus target and the keyboard flow breaks.
### Issue Context
- The ArrowLeft handler in `MultiSelectTrigger` only focuses `overflowBadgeRef`.
- The ArrowLeft-to-chip behavior in `MultiSelectedValues` is implemented only on the overflow wrapper, which only exists when `hiddenCount > 0`.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-71]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-173]
- packages/core/src/components/Dropdown/Dropdown.types.ts[29-34]
### Implementation guidance
- Add a ref to the last *visible* chip container and focus it when ArrowLeft is pressed from an empty input and there is no overflow badge.
- Example approach:
- In `MultiSelectTrigger`, create `const lastChipRef = useRef<HTMLDivElement>(null)`.
- Pass it via `getChipContainerProps` for the last selected chip (only when you know there is no overflow) and ensure the chip container is focusable.
- Update the ArrowLeft handler:
- If `overflowBadgeRef.current` exists, focus it (current behavior).
- Else `lastChipRef.current?.focus()`.
- Consider also handling `Backspace` similarly if that’s part of the intended keyboard contract for `interactiveChips`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Wrong close detection 🐞 Bug ≡ Correctness ⭐ New
Description
In useDropdownMultiCombobox the stateReducer treats changes.isOpen being undefined the same
as false via !changes.isOpen, so non-close transitions that omit isOpen can incorrectly force
inputValue back to closedInputValue while the menu is still open.
Code

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[R178-180]

+          if (!changes.isOpen && state.isOpen) {
+            return { ...changes, inputValue: closedInputValue };
+          }
Evidence
The reducer’s default branch uses a falsy check on changes.isOpen, which will also match
undefined, and then overwrites inputValue with closedInputValue even though state.isOpen is
true.

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[140-182]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`useDropdownMultiCombobox` uses a falsy check (`!changes.isOpen`) to infer that the menu is closing. Since `changes.isOpen` can be `undefined` when Downshift doesn’t include it in `changes`, the reducer can misclassify state updates as a close and overwrite `inputValue` unexpectedly.

### Issue Context
This logic runs in the new multi-combobox `stateReducer` default branch and affects both default multi-select and the new `textInput` mode.

### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[140-182]

### Suggested fix
Use a strict close check, e.g. `if (changes.isOpen === false && state.isOpen) { ... }`, instead of a falsy check. If you also need to handle explicit open transitions, handle `changes.isOpen === true` separately.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Object refs dropped 🐞 Bug ☼ Reliability ⭐ New
Description
MultiSelectedValues merges refs for chip containers but only forwards function refs from
getChipContainerProps; if a RefObject is provided, it never receives the element, breaking
external focus management that relies on object refs.
Code

packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[R82-91]

+      const extraProps = isVisible && getChipContainerProps ? getChipContainerProps(item, index) : {};
+      const { ref: extraRef, ...extraAttrs } = extraProps;

      return (
        <div
          key={`dropdown-chip-visible-${item.value}`}
-          ref={itemRefs[index]}
+          ref={el => {
+            (itemRefs[index] as React.MutableRefObject<HTMLDivElement | null>).current = el;
+            if (typeof extraRef === "function") extraRef(el);
+          }}
Evidence
The component destructures ref out of the extra props and only invokes it when it’s a function, so
object refs are silently ignored.

packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[13-24]
packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[79-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`MultiSelectedValues` ref-merging only handles callback refs (`typeof extraRef === "function"`). If callers provide a `RefObject` (e.g. `useRef()` / `createRef()`), it will be ignored and never populated.

### Issue Context
`getChipContainerProps` is typed as returning a generic props object and can reasonably include either callback refs or object refs.

### Fix Focus Areas
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[79-110]

### Suggested fix
Update the ref callback to handle both shapes:
- If `extraRef` is a function: `extraRef(el)`
- Else if `extraRef` is an object ref: `(extraRef as React.MutableRefObject<HTMLDivElement | null>).current = el`
Also consider passing `null` on unmount (`el === null`) for both ref types.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. onKeyDown prop overridden 🐞 Bug ≡ Correctness
Description
DropdownInput sets onKeyDown: externalKeyDown but then spreads multipleSelectionDropdownProps
afterward, so any colliding keys (notably onKeyDown) can overwrite the external handler. This
makes the new onKeyDown prop unreliable when multi-selection keyboard props are present.
Code

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[R45-51]

+  const preventKeyAction = interactiveChips ? !!(inputValue && inputValue.length > 0) : isOpen;
+  const multipleSelectionDropdownProps = getDropdownProps ? getDropdownProps({ preventKeyAction }) : {};
-  return (
+return (
<>
  {searchable ? (
    <BaseInput
Evidence
The code explicitly assigns onKeyDown: externalKeyDown and then applies
...multipleSelectionDropdownProps afterward, which will override earlier properties on key
collisions. The same block shows multipleSelectionDropdownProps is derived from
getDropdownProps, so it is intended to contribute keyboard handlers.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[40-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `DropdownInput`, the object passed to `getInputProps` sets `onKeyDown: externalKeyDown` and then spreads `...multipleSelectionDropdownProps` after it. If `multipleSelectionDropdownProps` contains an `onKeyDown`, it will overwrite `externalKeyDown`, so the newly added external handler may never run.
### Issue Context
`multipleSelectionDropdownProps` comes from `getDropdownProps({ preventKeyAction })` and is specifically used for keyboard behavior; it is a realistic source of an `onKeyDown` collision.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[42-60]
### Suggested fix approach
- Extract `multipleSelectionDropdownProps.onKeyDown` and compose it with `externalKeyDown` (call both, preserving existing behavior).
- Alternatively, spread `multipleSelectionDropdownProps` *before* setting `onKeyDown`, and set `onKeyDown` to a composed handler that invokes both.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (6)
7. interactiveChips lacks test coverage 📘 Rule violation ☼ Reliability
Description
The PR introduces interactiveChips keyboard-focus behavior (ArrowLeft focus transfer and chip
focus management) but adds no tests validating the new interaction and related accessibility
behavior. This increases regression risk for keyboard-only users and makes the a11y-focused behavior
change hard to verify.
Code

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[R41-72]

+    if (interactiveChips && searchable && !readOnly) {
+      if (selectedItems.length === 0) {
+        return <DropdownInput />;
+      }
+      return (
+        <div
+          className={styles.multiWrapper}
+          onKeyDown={e => {
+            if (
+              e.key === "ArrowLeft" &&
+              e.target instanceof HTMLInputElement &&
+              !e.target.value &&
+              overflowBadgeRef.current
+            ) {
+              overflowBadgeRef.current.focus();
+            }
+          }}
+        >
+          <MultiSelectedValues
+            disabled={disabled}
+            readOnly={readOnly}
+            selectedItems={selectedItems}
+            onRemove={item => contextOnOptionRemove?.(item)}
+            renderInput={() => <DropdownInput inputSize="small" fullWidth />}
+            getChipContainerProps={(item, index) =>
+              getSelectedItemProps?.({ selectedItem: item, index }) ?? {}
+            }
+            badgeRef={overflowBadgeRef}
+            minVisibleCount={minVisibleCount}
+          />
+        </div>
+      );
Evidence
interactiveChips introduces new keyboard handling and focus management in the trigger and overflow
counter, but the updated test suite shown focuses on textInput multi-select behaviors and does not
include any interactiveChips scenarios validating focus movement, keyboard navigation, or removal
behavior.

CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes
packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-72]
packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-171]
packages/core/src/components/Dropdown/tests/Dropdown.test.tsx[605-709]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`interactiveChips` adds new keyboard navigation/focus-management behavior but there are no unit tests asserting the behavior and its a11y-relevant outcomes.
## Issue Context
The feature adds ArrowLeft-based focus transfer from the input to the overflow badge/last chip, plus chip container props/ref wiring for focusability. This should be covered with tests to ensure real DOM focus movement and removal behavior continues to work.
## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-72]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-171]
- packages/core/src/components/Dropdown/__tests__/Dropdown.test.tsx[605-709]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. textInput unguarded in triggers 🐞 Bug ≡ Correctness
Description
MultiSelectTrigger renders ` whenever textInput is true, even though textInput` is documented
as only applying when searchable=true. When searchable=false, DropdownInput does not render
the input or chips, so selected values can disappear from the UI if consumers set textInput in an
unsupported configuration.
Code

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[R36-39]

+  const renderTriggerContent = () => {
+    if (textInput) {
+      return <DropdownInput />;
+    }
Evidence
The component applies textInput without checking searchable, but DropdownInput only renders
the actual input UI when searchable is true. This creates a code-path where a non-searchable
multi-select can render neither chips nor an input value.

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-40]
packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[50-88]
packages/core/src/components/Dropdown/Dropdown.types.ts[23-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`textInput` is documented as only applying when `searchable=true`, but the trigger rendering applies it unconditionally. In non-searchable multi-select, this can hide the selected values.
### Issue Context
- `MultiSelectTrigger` returns `<DropdownInput />` early when `textInput` is true.
- `DropdownInput` only renders `<BaseInput>` when `searchable` is true; otherwise it renders only placeholder text.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-40]
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[50-88]
### Implementation guidance
- Change the early return to `if (textInput && searchable) { ... }`.
- If `textInput && !searchable`, fall back to the default chips rendering path (or explicitly ignore `textInput`).
- (Optional) add a dev-time warning when `textInput` is set while `searchable=false`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Docs misstate aria-label 🐞 Bug ≡ Correctness
Description
The new MDX claims aria-label provides the accessible name when no visible label exists, but for
searchable Dropdown the focused combobox input does not consume aria-label (it’s applied to the
outer wrapper). This can lead consumers to ship an unnamed `` unless they use
label/inputAriaLabel (or the code is updated to forward aria-label to the input).
Code

packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[R80-84]

+| `label`          | Visible text label, programmatically associated with the input (`aria-labelledby`). | **Preferred.** Visible to everyone and announced by screen readers.                                       |
+| `aria-label`     | Accessible name when there is **no** visible `label`.                            | Use only when a visible label is not possible.                                                              |
+| `inputAriaLabel` | Accessible name applied specifically to the inner search input.                  | Useful when the input needs a name distinct from the field label.                                           |
+| `menuAriaLabel`  | Accessible name for the option list (`listbox`).                                 | Helps orient users when the menu opens.                                                                     |
+| `clearAriaLabel` | Accessible name for the clear (✕) button.                                        | **Important.** Without it the clear button is an icon-only control with no name — a 4.1.2 failure. Always set it when `clearable`. |
Evidence
The MDX explicitly instructs using aria-label as the naming fallback, but the component applies
aria-label to the wrapper div and does not use it for the searchable input’s accessible name,
which instead depends on inputAriaLabel/label wiring.

packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[76-84]
packages/core/src/components/Dropdown/components/DropdownBase/DropdownBase.tsx[38-55]
packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[35-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new documentation states that `aria-label` provides the accessible name for the searchable single-select combobox when no visible `label` exists. In the current implementation, `aria-label` is applied to the outer Dropdown wrapper, while the actual focusable combobox input’s `aria-label` is derived from `inputAriaLabel` (or a label association), so `aria-label` alone won’t reliably name the combobox.
## Issue Context
This is an accessibility-focused change and the docs are intended to be prescriptive. If consumers follow the MDX guidance and provide only `aria-label` without `label`/`inputAriaLabel`, the combobox input may still be effectively unnamed.
## Fix
Do one (or both):
1) **Docs-only fix:** Update the MDX to clarify that for **searchable** Dropdown, the accessible name must come from `label` or `inputAriaLabel` (and that `aria-label` currently labels the wrapper, not the input).
2) **Code alignment fix (preferred):** Forward `aria-label` to the actual searchable input as a fallback when `inputAriaLabel` isn’t provided (e.g., `aria-label: inputAriaLabel ?? ariaLabel ?? …`).
## Fix Focus Areas
- packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[76-84]
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[35-46]
- packages/core/src/components/Dropdown/components/DropdownBase/DropdownBase.tsx[38-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Stories lack accessible name 🐞 Bug ≡ Correctness
Description
Several new searchable single-select stories render Dropdown without label, aria-label, or
inputAriaLabel, producing unnamed combobox examples on an accessibility-focused page. This
contradicts the page’s own guidance and can mislead consumers and/or fail a11y checks on docs pages.
Code

packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.stories.tsx[R407-414]

+            <Dropdown
+              id="searchable-single-groups"
+              options={groupedOptions}
+              searchable
+              placeholder="Search a team"
+              maxMenuHeight={200}
+              clearAriaLabel="Clear"
+            />
Evidence
The stories shown omit naming props despite the MDX explicitly stating the field must have an
accessible name; these examples currently only provide placeholder text.

packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.stories.tsx[402-445]
packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.stories.tsx[533-560]
packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[76-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Some Storybook examples on the new “Searchable single select — Accessibility” page render a searchable `Dropdown` without providing any accessible name (`label`, `aria-label`, or `inputAriaLabel`). Placeholder text is not a valid accessible name.
## Issue Context
This page is meant to document accessibility behavior and recommended props. Unnamed examples undermine that goal and may also cause automated accessibility checks to fail for docs.
## Fix
For each searchable Dropdown instance in these stories, add either:
- a visible `label`, or
- an `inputAriaLabel` / `aria-label` (depending on the intended guidance),
so that every example is programmatically named.
## Fix Focus Areas
- packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.stories.tsx[402-445]
- packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.stories.tsx[533-560]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. valueRenderer ignored when searchable 🐞 Bug ≡ Correctness
Description
In searchable single-select, the selected-value overlay is no longer rendered, so valueRenderer
and selected-item startElement visuals (icon/avatar/custom ReactNode) are silently dropped and
replaced by plain text inside the input. This is a backward-incompatible UI/API behavior change for
consumers relying on valueRenderer to render the selected value in single-select.
Code

packages/core/src/components/Dropdown/components/Trigger/SingleSelectTrigger.tsx[R43-49]

+        {/* Non-searchable single select shows the selection via this overlay. In searchable mode the
+            selected value lives inside the input itself, so the overlay must not render. */}
+        {!searchable && selectedItem && (
+          <div className={cx(styles.selectedItem, getStyle(styles, size))}>
 <BaseItem
   component="div"
   itemRenderer={valueRenderer}
Evidence
valueRenderer is a documented single-select prop, but after the change it is only applied inside
an overlay that is now gated behind !searchable. Searchable mode renders only a text `` value
(inputValue) and never uses valueRenderer, so custom selected-value rendering is lost.

packages/core/src/components/Dropdown/Dropdown.types.ts[45-74]
packages/core/src/components/Dropdown/components/Trigger/SingleSelectTrigger.tsx[11-60]
packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[35-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Searchable single-select no longer renders the selected-value overlay, which was the only place `valueRenderer` and selected-item `startElement` were rendered. As a result, consumers using `valueRenderer` (or expecting selected icon/avatar) lose that visual rendering in searchable mode.
## Issue Context
- `valueRenderer` is part of the public single-select API (not scoped to `searchable: false`).
- After this PR, searchable mode shows only the label string in the input, with no mechanism to render custom selected-value content.
## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/SingleSelectTrigger.tsx[43-59]
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[35-56]
- packages/core/src/components/Dropdown/Dropdown.types.ts[45-74]
## Implementation direction
- Keep the selected label in the input for accessibility, but restore visual rendering for `valueRenderer`/`startElement` in searchable mode.
- Option A: render a visual adornment (icon/avatar/custom node) next to the input using BaseInput’s adornment APIs (if available), driven by `selectedItem`/`valueRenderer`.
- Option B: reintroduce a visual-only overlay for searchable mode that is explicitly `aria-hidden="true"` and does not affect what AT reads, while the input still contains the selected label.
- Add/adjust tests to cover that `valueRenderer` affects the rendered selection in searchable mode (while ensuring the input value remains the label).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Filter reset ignores input 🐞 Bug ≡ Correctness
Description
useDropdownCombobox clears the filter state on menu close even when the user typed a query without
selecting, so the input can still show the query while the option list resets to unfiltered on
reopen (and in boxMode where options keep rendering). This desynchronizes the displayed options from
the visible input text and can mislead users while browsing/changing selections.
Code

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[R67-73]

onIsOpenChange: ({ isOpen }) => {
+      // Reset the text filter when the menu closes so reopening always shows the full option list,
+      // even though the input keeps displaying the selected item's label.
+      if (!isOpen) {
+        filterOptions("");
+      }
isOpen ? onMenuClose?.() : onMenuOpen?.();
Evidence
The hook resets the filter state on close (filterOptions("")) but filtering updates are otherwise
only applied on InputChange, so closing/reopening can reset filteredOptions while inputValue
remains whatever the user typed. useDropdownFiltering confirms the filter state is stored
separately from the input, and DropdownInput renders from inputValue, making divergence visible;
MenuList renders options even when closed in boxMode, amplifying the mismatch.

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[67-86]
packages/core/src/components/Dropdown/hooks/useDropdownFiltering.ts[13-24]
packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[33-56]
packages/core/src/components/Dropdown/components/Menu/MenuList.tsx[24-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useDropdownCombobox` resets the filtering state via `filterOptions("")` when the menu closes, but it does not also clear/sync the combobox `inputValue`. If the user typed a search query and then closes the menu without selecting, the next open can show the full, unfiltered list while the input still displays the typed query.
## Issue Context
- Filtering is driven by `filterValue` in `useDropdownFiltering`, which is independent from Downshift's `inputValue`.
- After this PR, filtering only updates on `InputChange`, so reopening the menu does not re-apply filtering based on the current `inputValue`.
- In boxMode, options are rendered regardless of `isOpen`, making the mismatch especially visible.
## Fix approach (choose one)
1) **Conditional reset:** Only reset the filter on close when the input is showing the selected label (or when a selection exists), but **do not** reset when there is no selection and the user has typed a query.
2) **Sync on close:** If you always reset the filter on close, also clear/sync the input value on close (e.g., clear to `""` when no selection; restore to `selectedItem.label` when a selection exists).
3) **Sync on open:** On open, set the filter value based on the current input value unless it equals the selected label (in which case keep filter empty to show full list).
## Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[67-86]
- packages/core/src/components/Dropdown/hooks/useDropdownFiltering.ts[13-24]
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[33-56]
- packages/core/src/components/Dropdown/components/Menu/MenuList.tsx[24-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit f279286

Results up to commit N/A


🐞 Bugs (8) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)


Action required
1. Unused hook parameter 🐞 Bug ⚙ Maintainability
Description
useDropdownMultiCombobox adds an interactiveChips parameter but never uses it, which violates the
core package’s TypeScript unused-vars ESLint rule and can fail CI/linting. This also indicates the
hook signature doesn’t match its implemented behavior.
Code

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[R23-26]

+  id?: string,
+  onOptionRemove?: (option: T) => void,
+  textInput?: boolean,
+  interactiveChips?: boolean
Evidence
The hook signature includes interactiveChips but the implementation’s control flow only uses
textInput (e.g., enableToggle = textInput), leaving interactiveChips unused. Core ESLint
config marks unused variables/args as an error for TS/TSX files, so this is a merge-blocking lint
issue when lint runs.

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[7-36]
packages/core/.eslintrc.cjs[97-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useDropdownMultiCombobox` declares `interactiveChips?: boolean` but never references it. In `packages/core`, `@typescript-eslint/no-unused-vars` is configured as an error for TS/TSX files, so this can break lint/CI.
### Issue Context
`interactiveChips` is already consumed in UI (Trigger) and context; it does not appear to be needed inside this hook. The cleanest fix is to remove the argument from the hook signature and from the call site(s). If you want to keep it for future work, rename it to `_interactiveChips` to satisfy the lint rule.
### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[7-36]
- packages/core/src/components/Dropdown/modes/DropdownMultiComboboxController.tsx[47-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Chip focus fails without overflow 🐞 Bug ≡ Correctness
Description
In interactiveChips mode, ArrowLeft from the input only focuses the overflow (+N) badge; when
there is no overflow badge (all chips visible), ArrowLeft does nothing so keyboard users can’t reach
chips via the documented navigation. This contradicts the interactiveChips prop contract that
ArrowLeft/Backspace from the input moves focus to the last chip.
Code

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[R46-57]

+        <div
+          className={styles.multiWrapper}
+          onKeyDown={e => {
+            if (
+              e.key === "ArrowLeft" &&
+              e.target instanceof HTMLInputElement &&
+              !e.target.value &&
+              overflowBadgeRef.current
+            ) {
+              overflowBadgeRef.current.focus();
+            }
+          }}
Evidence
The code explicitly gates focus transfer on the presence of overflowBadgeRef.current, and the only
other ArrowLeft focus transfer logic lives on the overflow-counter wrapper that is rendered only
when hiddenCount > 0. The prop’s own documentation states ArrowLeft/Backspace should move focus to
the last chip, which isn’t satisfied when there’s no overflow.

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-57]
packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-173]
packages/core/src/components/Dropdown/Dropdown.types.ts[29-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`interactiveChips` documents that ArrowLeft/Backspace from the input moves focus to the last chip, but the current implementation only moves focus to the overflow badge when it exists. When `hiddenCount===0` (no overflow), there is no focus target and the keyboard flow breaks.
### Issue Context
- The ArrowLeft handler in `MultiSelectTrigger` only focuses `overflowBadgeRef`.
- The ArrowLeft-to-chip behavior in `MultiSelectedValues` is implemented only on the overflow wrapper, which only exists when `hiddenCount > 0`.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-71]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-173]
- packages/core/src/components/Dropdown/Dropdown.types.ts[29-34]
### Implementation guidance
- Add a ref to the last *visible* chip container and focus it when ArrowLeft is pressed from an empty input and there is no overflow badge.
- Example approach:
- In `MultiSelectTrigger`, create `const lastChipRef = useRef<HTMLDivElement>(null)`.
- Pass it via `getChipContainerProps` for the last selected chip (only when you know there is no overflow) and ensure the chip container is focusable.
- Update the ArrowLeft handler:
  - If `overflowBadgeRef.current` exists, focus it (current behavior).
  - Else `lastChipRef.current?.focus()`.
- Consider also handling `Backspace` similarly if that’s part of the intended keyboard contract for `interactiveChips`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. onKeyDown prop overridden 🐞 Bug ≡ Correctness
Description
DropdownInput sets onKeyDown: externalKeyDown but then spreads multipleSelectionDropdownProps
afterward, so any colliding keys (notably onKeyDown) can overwrite the external handler. This
makes the new onKeyDown prop unreliable when multi-selection keyboard props are present.
Code

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[R45-51]

+  const preventKeyAction = interactiveChips ? !!(inputValue && inputValue.length > 0) : isOpen;
+  const multipleSelectionDropdownProps = getDropdownProps ? getDropdownProps({ preventKeyAction }) : {};
-  return (
+return (
  <>
    {searchable ? (
      <BaseInput
Evidence
The code explicitly assigns onKeyDown: externalKeyDown and then applies
...multipleSelectionDropdownProps afterward, which will override earlier properties on key
collisions. The same block shows multipleSelectionDropdownProps is derived from
getDropdownProps, so it is intended to contribute keyboard handlers.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[40-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `DropdownInput`, the object passed to `getInputProps` sets `onKeyDown: externalKeyDown` and then spreads `...multipleSelectionDropdownProps` after it. If `multipleSelectionDropdownProps` contains an `onKeyDown`, it will overwrite `externalKeyDown`, so the newly added external handler may never run.
### Issue Context
`multipleSelectionDropdownProps` comes from `getDropdownProps({ preventKeyAction })` and is specifically used for keyboard behavior; it is a realistic source of an `onKeyDown` collision.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[42-60]
### Suggested fix approach
- Extract `multipleSelectionDropdownProps.onKeyDown` and compose it with `externalKeyDown` (call both, preserving existing behavior).
- Alternatively, spread `multipleSelectionDropdownProps` *before* setting `onKeyDown`, and set `onKeyDown` to a composed handler that invokes both.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. interactiveChips lacks test coverage 📘 Rule violation ☼ Reliability
Description
The PR introduces interactiveChips keyboard-focus behavior (ArrowLeft focus transfer and chip
focus management) but adds no tests validating the new interaction and related accessibility
behavior. This increases regression risk for keyboard-only users and makes the a11y-focused behavior
change hard to verify.
Code

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[R41-72]

+    if (interactiveChips && searchable && !readOnly) {
+      if (selectedItems.length === 0) {
+        return <DropdownInput />;
+      }
+      return (
+        <div
+          className={styles.multiWrapper}
+          onKeyDown={e => {
+            if (
+              e.key === "ArrowLeft" &&
+              e.target instanceof HTMLInputElement &&
+              !e.target.value &&
+              overflowBadgeRef.current
+            ) {
+              overflowBadgeRef.current.focus();
+            }
+          }}
+        >
+          <MultiSelectedValues
+            disabled={disabled}
+            readOnly={readOnly}
+            selectedItems={selectedItems}
+            onRemove={item => contextOnOptionRemove?.(item)}
+            renderInput={() => <DropdownInput inputSize="small" fullWidth />}
+            getChipContainerProps={(item, index) =>
+              getSelectedItemProps?.({ selectedItem: item, index }) ?? {}
+            }
+            badgeRef={overflowBadgeRef}
+            minVisibleCount={minVisibleCount}
+          />
+        </div>
+      );
Evidence
interactiveChips introduces new keyboard handling and focus management in the trigger and overflow
counter, but the updated test suite shown focuses on textInput multi-select behaviors and does not
include any interactiveChips scenarios validating focus movement, keyboard navigation, or removal
behavior.

CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes: CLAUDE.md: Tests Must Validate Real Behavior and Accessibility Attributes
packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-72]
packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-171]
packages/core/src/components/Dropdown/tests/Dropdown.test.tsx[605-709]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`interactiveChips` adds new keyboard navigation/focus-management behavior but there are no unit tests asserting the behavior and its a11y-relevant outcomes.
## Issue Context
The feature adds ArrowLeft-based focus transfer from the input to the overflow badge/last chip, plus chip container props/ref wiring for focusability. This should be covered with tests to ensure real DOM focus movement and removal behavior continues to work.
## Fix Focus Areas
- packages/core/src/components/Dropdown/compone...

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Dropdown: keep selected label in input for searchable single-select (WCAG 4.1.2)
🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Keep searchable single-select value inside the input so screen readers announce the selection
  (WCAG 4.1.2).
• Add multi-select accessibility modes: text summary in input or keyboard-navigable interactive
  chips.
• Expand Storybook docs and update tests to match the new accessible behaviors.
Diagram
graph TD
  A["Dropdown (core)"] --> B["SingleSelectTrigger"] --> C("useDropdownCombobox")
  A["Dropdown (core)"] --> D["MultiSelectTrigger"] --> E("useDropdownMultiCombobox") --> F["MultiSelectedValues"]
  D["MultiSelectTrigger"] --> G["DropdownInput"]
  A["Dropdown (core)"] --> H["Docs & tests"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep overlay UI but mirror value via aria-valuetext/hidden input
  • ➕ Preserves rich selected rendering (icons/avatars/valueRenderer) while still announcing a value
  • ➕ Potentially smaller visible-behavior change for existing consumers
  • ➖ More complex and fragile: must keep overlay, hidden text, and Downshift state perfectly synchronized
  • ➖ Higher risk of regressions across screen readers; still diverges from typical combobox patterns
2. Multi-select: keep chips, add offscreen live summary for AT
  • ➕ Keeps chip UI unchanged while exposing a single summarized value to assistive tech
  • ➕ Doesn’t require rewriting inputValue/stateReducer behaviors
  • ➖ Live region behavior varies across AT; announcements can be noisy or missed
  • ➖ Still leaves the focused input value technically empty, which can confuse some AT workflows
3. Adopt a higher-level combobox implementation (e.g., Zag.js/Ark UI patterns)
  • ➕ Standardizes behaviors (open/reset/filtering) with well-tested a11y primitives
  • ➕ Reduces custom reducer logic over time
  • ➖ Much larger migration with API/behavior churn
  • ➖ Not appropriate for a targeted fix PR

Recommendation: Current approach is the best trade-off for single-select: rely on Downshift’s default of writing the selected label into the input, which directly satisfies WCAG 4.1.2 and matches common combobox guidance. For multi-select, offering explicit modes (textInput for value announcement; interactiveChips for keyboard-operable chip removal) is a pragmatic, opt-in path without breaking existing chip layouts.

Grey Divider

File Changes

Enhancement (7)
Dropdown.types.ts Add multi-select a11y props: textInput and interactiveChips +12/-0

Add multi-select a11y props: textInput and interactiveChips

• Introduces new multi-select-only props to control how selections are exposed: a comma-separated text summary in the input (textInput) or always-visible, keyboard-navigable chips (interactiveChips). Comments clarify WCAG 4.1.2 motivation and searchable-only scope.

packages/core/src/components/Dropdown/Dropdown.types.ts


MultiSelectedValues.tsx Enable focusable chip containers and overflow badge ref +20/-3

Enable focusable chip containers and overflow badge ref

• Adds hooks to pass extra props/ref into visible chip wrappers for external focus/keyboard management. Implements ArrowLeft behavior from overflow badge to focus the last visible chip and forwards a ref to the overflow badge chip.

packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx


DropdownInput.tsx Support external key handling/ref and refine preventKeyAction for chip nav +22/-6

Support external key handling/ref and refine preventKeyAction for chip nav

• Allows MultiSelectTrigger to pass onKeyDown and an input ref into the BaseInput. Changes preventKeyAction logic so interactive chip navigation is not permanently suppressed when the menu remains open, only when the input contains text.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx


MultiSelectTrigger.tsx Add textInput and interactiveChips trigger rendering modes +89/-38

Add textInput and interactiveChips trigger rendering modes

• Refactors trigger rendering into mode-aware paths: textInput (no chips, summary in input), interactiveChips (chips focusable + ArrowLeft navigation from input), and the existing default chips mode. Wires getSelectedItemProps and overflow badge focus to support keyboard interactions.

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx


DropdownContext.types.ts Expose selected-item props and multi-select a11y flags in context +3/-0

Expose selected-item props and multi-select a11y flags in context

• Extends the dropdown context to include getSelectedItemProps for chip focus/props wiring, plus textInput and interactiveChips flags. Enables trigger/components to coordinate behavior without prop drilling.

packages/core/src/components/Dropdown/context/DropdownContext.types.ts


useDropdownMultiCombobox.ts Add textInput/interactiveChips behaviors for multi-select combobox +95/-14

Add textInput/interactiveChips behaviors for multi-select combobox

• Implements textInput initial value as comma-joined selected labels and restores that value on blur/close. Adds toggle handling for repeat-click selection changes using a ref to survive reducer resets, filters only on user typing, and calls onOptionRemove for keyboard-driven deletion; preserves original placeholder-clearing behavior when not in textInput mode.

packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts


DropdownMultiComboboxController.tsx Plumb textInput/interactiveChips and selected-item props through controller +13/-4

Plumb textInput/interactiveChips and selected-item props through controller

• Passes new a11y mode props and onOptionRemove into the multi-combobox hook and exposes getSelectedItemProps in context. Ensures triggers can drive keyboard/focus behavior consistently.

packages/core/src/components/Dropdown/modes/DropdownMultiComboboxController.tsx


Bug fix (2)
SingleSelectTrigger.tsx Disable selected-value overlay for searchable single select +4/-12

Disable selected-value overlay for searchable single select

• Removes the overlay rendering when searchable=true so the selected label can live inside the input. Keeps overlay behavior for non-searchable single select (where the input remains empty).

packages/core/src/components/Dropdown/components/Trigger/SingleSelectTrigger.tsx


useDropdownCombobox.ts Keep selected label in input and reset filter on close (single-select) +17/-7

Keep selected label in input and reset filter on close (single-select)

• Seeds initialInputValue from selectedItem.label so defaultValue/controlled value is visible on mount. Filters only on real user input changes and resets the filter when the menu closes; removes the reducer override that forced inputValue to null on selection/blur.

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts


Refactor (1)
Trigger.module.scss Remove obsolete overlay positioning and faded styling +0/-10

Remove obsolete overlay positioning and faded styling

• Deletes CSS for the old searchable single-select overlay positioning and the 'faded' style used on focus. Aligns styling with the new in-input selected value behavior.

packages/core/src/components/Dropdown/components/Trigger/Trigger.module.scss


Tests (1)
Dropdown.test.tsx Rewrite tests for selected-value-in-input and new multi-select modes +73/-82

Rewrite tests for selected-value-in-input and new multi-select modes

• Updates single-select searchable assertions to validate that the input retains the selected label (no overlay/fade behavior). Adjusts multi-select tests to cover textInput summary behavior, deselection via re-click, filter/list behaviors, and boxMode compatibility.

packages/core/src/components/Dropdown/tests/Dropdown.test.tsx


Documentation (4)
DropdownMultiSelectA11y.mdx Add MDX guidance for multi-select accessibility modes +34/-0

Add MDX guidance for multi-select accessibility modes

• Documents why chip-only multi-select can announce as blank and proposes two solutions: textInput (summary in input) and interactiveChips (keyboard-focusable chips). Embeds the corresponding Storybook canvases and explains recommended usage.

packages/docs/src/pages/components/Dropdown/DropdownMultiSelectA11y.mdx


DropdownMultiSelectA11y.stories.tsx Add Storybook stories for textInput and interactiveChips multi-select +76/-0

Add Storybook stories for textInput and interactiveChips multi-select

• Introduces two focused stories demonstrating the new multi-select accessibility modes with actions wired for change/select/remove/clear. Provides an easy sandbox for validating screen reader and keyboard behavior.

packages/docs/src/pages/components/Dropdown/DropdownMultiSelectA11y.stories.tsx


DropdownSearchableSingleSelect.mdx Add searchable single-select accessibility reference +106/-0

Add searchable single-select accessibility reference

• Creates a dedicated a11y reference explaining the shift from overlay-selected display to selected label inside the input, including the WCAG 4.1.2 rationale. Documents behavior on reopen/filtering and clarifies trade-offs (text-only collapsed value).

packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx


DropdownSearchableSingleSelect.stories.tsx Add comprehensive searchable single-select Storybook page +564/-0

Add comprehensive searchable single-select Storybook page

• Adds a full story suite covering overview, sizes, states, default/controlled values, icons/avatars, end elements, custom valueRenderer trade-offs, groups/sticky/dividers, tooltips, clearable/max-height, and custom filtering/no-options messaging.

packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.stories.tsx


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Analysis

✅ No bundle size changes detected.

Unchanged Components
Component Base PR Diff
@vibe/button 17.31KB 17.28KB -36B 🟢
@vibe/clickable 5.97KB 5.97KB +8B 🔺
@vibe/dialog 52.19KB 52.18KB -13B 🟢
@vibe/icon-button 66.25KB 66.08KB -176B 🟢
@vibe/icon 12.93KB 12.87KB -59B 🟢
@vibe/layer 2.97KB 2.96KB -10B 🟢
@vibe/layout 9.86KB 9.82KB -43B 🟢
@vibe/loader 5.67KB 5.65KB -28B 🟢
@vibe/tooltip 61.34KB 61.29KB -47B 🟢
@vibe/typography 63.45KB 63.46KB +5B 🔺
Accordion 6.3KB 6.29KB -12B 🟢
AccordionItem 66.5KB 66.46KB -41B 🟢
AlertBanner 70.91KB 70.81KB -106B 🟢
AlertBannerButton 18.79KB 18.78KB -16B 🟢
AlertBannerLink 15.24KB 15.25KB +10B 🔺
AlertBannerText 64.03KB 63.93KB -100B 🟢
AttentionBox 74.42KB 74.28KB -142B 🟢
Avatar 66.8KB 66.77KB -24B 🟢
AvatarGroup 93.35KB 93.33KB -19B 🟢
Badge 43.18KB 43.21KB +33B 🔺
BreadcrumbItem 64.74KB 64.72KB -20B 🟢
BreadcrumbMenu 68.64KB 68.62KB -23B 🟢
BreadcrumbMenuItem 77.07KB 77.04KB -32B 🟢
BreadcrumbsBar 5.68KB 5.69KB +8B 🔺
ButtonGroup 68.41KB 68.39KB -28B 🟢
Checkbox 66.95KB 66.94KB -16B 🟢
Chips 75.13KB 75.03KB -98B 🟢
ColorPicker 74.53KB 74.43KB -104B 🟢
ColorPickerContent 73.75KB 73.83KB +78B 🔺
Combobox 84.08KB 84.03KB -55B 🟢
Counter 42.21KB 42.23KB +14B 🔺
DatePicker 112.45KB 112.85KB +413B 🔺
Divider 5.45KB 5.44KB -10B 🟢
Dropdown 95.33KB 95.76KB +442B 🔺
EditableHeading 66.59KB 66.65KB +60B 🔺
EditableText 66.49KB 66.54KB +46B 🔺
EmptyState 70.43KB 70.42KB -7B 🟢
ExpandCollapse 66.28KB 66.24KB -37B 🟢
FormattedNumber 5.83KB 5.84KB +7B 🔺
GridKeyboardNavigationContext 4.66KB 4.65KB -16B 🟢
HiddenText 5.42KB 5.39KB -35B 🟢
Info 72.15KB 72.06KB -89B 🟢
Label 68.65KB 68.67KB +15B 🔺
Link 14.92KB 14.88KB -40B 🟢
List 72.92KB 72.87KB -55B 🟢
ListItem 65.54KB 65.53KB -10B 🟢
ListItemAvatar 66.9KB 66.92KB +21B 🔺
ListItemIcon 13.97KB 14.02KB +54B 🔺
ListTitle 65.08KB 65.06KB -25B 🟢
Menu 8.63KB 8.65KB +17B 🔺
MenuDivider 5.55KB 5.56KB +3B 🔺
MenuGridItem 7.21KB 7.19KB -12B 🟢
MenuItem 76.99KB 76.97KB -17B 🟢
MenuItemButton 70.02KB 70.15KB +135B 🔺
MenuTitle 65.36KB 65.27KB -84B 🟢
MenuButton 66.14KB 66.21KB +71B 🔺
Modal 79.2KB 79.13KB -78B 🟢
ModalContent 4.73KB 4.71KB -12B 🟢
ModalHeader 65.88KB 65.79KB -96B 🟢
ModalMedia 7.53KB 7.47KB -56B 🟢
ModalFooter 67.75KB 67.7KB -52B 🟢
ModalFooterWizard 68.61KB 68.61KB 0B ➖
ModalBasicLayout 8.92KB 8.94KB +15B 🔺
ModalMediaLayout 8.08KB 8.06KB -18B 🟢
ModalSideBySideLayout 6.3KB 6.31KB +13B 🔺
MultiStepIndicator 53KB 52.93KB -80B 🟢
NumberField 72.84KB 72.85KB +14B 🔺
ProgressBar 7.37KB 7.34KB -39B 🟢
RadioButton 65.88KB 65.95KB +69B 🔺
Search 70.57KB 70.71KB +136B 🔺
Skeleton 6.02KB 6.01KB -8B 🟢
Slider 73.95KB 73.81KB -147B 🟢
SplitButton 66.49KB 66.53KB +38B 🔺
SplitButtonMenu 8.76KB 8.78KB +17B 🔺
Steps 71.39KB 71.34KB -56B 🟢
Table 7.25KB 7.24KB -14B 🟢
TableBody 66.77KB 66.78KB +10B 🔺
TableCell 65.28KB 65.19KB -96B 🟢
TableContainer 5.34KB 5.32KB -24B 🟢
TableHeader 5.66KB 5.63KB -25B 🟢
TableHeaderCell 72.25KB 72.17KB -78B 🟢
TableRow 5.56KB 5.54KB -13B 🟢
TableRowMenu 68.89KB 68.8KB -87B 🟢
TableVirtualizedBody 71.49KB 71.43KB -67B 🟢
Tab 64.04KB 64.05KB +9B 🔺
TabList 8.91KB 8.85KB -64B 🟢
TabPanel 5.3KB 5.29KB -12B 🟢
TabPanels 5.85KB 5.84KB -13B 🟢
TabsContext 5.47KB 5.51KB +33B 🔺
TextArea 66.26KB 66.39KB +125B 🔺
TextField 69.44KB 69.4KB -46B 🟢
TextWithHighlight 64.3KB 64.26KB -44B 🟢
ThemeProvider 4.37KB 4.36KB -8B 🟢
Tipseen 71.2KB 71.1KB -98B 🟢
TipseenContent 71.63KB 71.71KB +82B 🔺
TipseenMedia 71.37KB 71.38KB +13B 🔺
TipseenWizard 73.86KB 73.86KB +8B 🔺
Toast 74.06KB 74.08KB +17B 🔺
ToastButton 18.64KB 18.61KB -33B 🟢
ToastLink 15.08KB 15.09KB +15B 🔺
Toggle 66.62KB 66.64KB +19B 🔺
TransitionView 5.44KB 5.44KB -1B 🟢
VirtualizedGrid 12.53KB 12.53KB +3B 🔺
VirtualizedList 12.26KB 12.28KB +20B 🔺
List (Next) 8.2KB 8.15KB -48B 🟢
ListItem (Next) 69.97KB 69.94KB -38B 🟢
ListTitle (Next) 65.33KB 65.34KB +14B 🔺

📊 Summary:

  • Total Base Size: 4.76MB
  • Total PR Size: 4.75MB
  • Total Difference: 1KB

Comprehensive story page covering all searchable single-select variants:
overview, sizes, states, default/controlled value, icons & avatars, groups,
tooltips, clearable/max-height, and custom filter / empty message. The
default-value story demonstrates the selected value living inside the input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 6edf9ba

Single-page accessibility reference covering the selected-value-in-input
behavior, WCAG criteria, keyboard interaction, screen reader output, and the
accessibility-relevant props (naming, state, feedback). Excludes layout props
like size that do not affect accessibility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 3417b82

Convert the plain .md (which Storybook does not pick up) into an .mdx docs
page for the Searchable single select group, with the live examples embedded.
Trim to accessibility essentials: core selected-value-in-input behavior,
WCAG 4.1.2, and the accessibility-relevant props. Dropped the generic keyboard
and screen-reader sections and the broader WCAG list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 11b7f86

…single select

The .selectedItem overlay was previously hidden for searchable mode only via
the `!inputValue` guard, so it reappeared and coexisted with the input value
whenever the input text was cleared while a selection remained. Gate the
overlay on `!searchable` instead — for searchable single select the value
lives inside the input, so the overlay must never render. Removes the now-dead
`faded`/`hasSelected` classes and their styles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 127c22d

…-off examples

Add a clear "What changed" before/after section to the searchable single
select accessibility page, and stories that demonstrate the text-only
collapsed selected value: preselected start elements (icons/avatars), end
elements (trailing icon, suffix/hint), and a custom valueRenderer that is not
applied to the searchable selected display. Remove the Do/Don't section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit d0ea70b

MDX parses {curly braces} as JS expressions; "{selected label}" is not valid
JS and broke the Storybook/Chromatic preview build with an acorn parse error.
Replace it with plain text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Looking for bugs?

Check back in a few minutes. An AI review agent is analyzing this pull request.

rivka-ungar and others added 3 commits June 12, 2026 01:01
textInput: selected items are shown as a comma-separated summary in the
input (WCAG 4.1.2 — exposes value to assistive technologies on focus).

interactiveChips: chips stay visible alongside the input; keyboard nav
via getSelectedItemProps — ArrowLeft/Right moves between chips,
Backspace/Delete removes the focused chip, ArrowLeft from an empty input
navigates to the last chip or the +N overflow badge. Chips overflow uses
the existing useItemsOverflow hook for the +N count badge.

Both modes keep the menu open on selection and support toggle (re-click
to deselect). Default chip mode behavior is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Storybook MDX page and stories for the textInput and interactiveChips
props explaining how each addresses the WCAG 4.1.2 gap in default
multi-select (empty input announced as blank by screen readers).

Covers:
- Side-by-side comparison of all three modes
- textInput: comma-separated value in the input, exposed on mount,
  controlled usage, trade-offs
- interactiveChips: keyboard navigation table, overflow (+N badge),
  trade-offs
- Decision guide for choosing between modes
- Common a11y props table (label, clearAriaLabel, error, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 4fcb1dc

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 1769c1d

Comment on lines +46 to +57
<div
className={styles.multiWrapper}
onKeyDown={e => {
if (
e.key === "ArrowLeft" &&
e.target instanceof HTMLInputElement &&
!e.target.value &&
overflowBadgeRef.current
) {
overflowBadgeRef.current.focus();
}
}}

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.

Action required

3. Chip focus fails without overflow 🐞 Bug ≡ Correctness

In interactiveChips mode, ArrowLeft from the input only focuses the overflow (+N) badge; when
there is no overflow badge (all chips visible), ArrowLeft does nothing so keyboard users can’t reach
chips via the documented navigation. This contradicts the interactiveChips prop contract that
ArrowLeft/Backspace from the input moves focus to the last chip.
Agent Prompt
### Issue description
`interactiveChips` documents that ArrowLeft/Backspace from the input moves focus to the last chip, but the current implementation only moves focus to the overflow badge when it exists. When `hiddenCount===0` (no overflow), there is no focus target and the keyboard flow breaks.

### Issue Context
- The ArrowLeft handler in `MultiSelectTrigger` only focuses `overflowBadgeRef`.
- The ArrowLeft-to-chip behavior in `MultiSelectedValues` is implemented only on the overflow wrapper, which only exists when `hiddenCount > 0`.

### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[41-71]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[129-173]
- packages/core/src/components/Dropdown/Dropdown.types.ts[29-34]

### Implementation guidance
- Add a ref to the last *visible* chip container and focus it when ArrowLeft is pressed from an empty input and there is no overflow badge.
  - Example approach:
    - In `MultiSelectTrigger`, create `const lastChipRef = useRef<HTMLDivElement>(null)`.
    - Pass it via `getChipContainerProps` for the last selected chip (only when you know there is no overflow) and ensure the chip container is focusable.
    - Update the ArrowLeft handler:
      - If `overflowBadgeRef.current` exists, focus it (current behavior).
      - Else `lastChipRef.current?.focus()`.
- Consider also handling `Backspace` similarly if that’s part of the intended keyboard contract for `interactiveChips`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +23 to +26
id?: string,
onOptionRemove?: (option: T) => void,
textInput?: boolean,
interactiveChips?: boolean

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.

Action required

1. Unused hook parameter 🐞 Bug ⚙ Maintainability

useDropdownMultiCombobox adds an interactiveChips parameter but never uses it, which violates the
core package’s TypeScript unused-vars ESLint rule and can fail CI/linting. This also indicates the
hook signature doesn’t match its implemented behavior.
Agent Prompt
### Issue description
`useDropdownMultiCombobox` declares `interactiveChips?: boolean` but never references it. In `packages/core`, `@typescript-eslint/no-unused-vars` is configured as an error for TS/TSX files, so this can break lint/CI.

### Issue Context
`interactiveChips` is already consumed in UI (Trigger) and context; it does not appear to be needed inside this hook. The cleanest fix is to remove the argument from the hook signature and from the call site(s). If you want to keep it for future work, rename it to `_interactiveChips` to satisfy the lint rule.

### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[7-36]
- packages/core/src/components/Dropdown/modes/DropdownMultiComboboxController.tsx[47-84]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

…ipleSelection onStateChange

UseMultipleSelectionStateChange has no selectedItem property — diff old vs new selectedItems array instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit f279286

@github-actions

Copy link
Copy Markdown
Contributor

A new prerelease version of this PR has been published! 🎉
To use this prerelease version, install the needed packages in your project:

@vibe/core@4.4.0-alpha-26200.0
@vibe/docs@4.0.11-alpha-26200.0

Comment on lines +87 to 88
initialInputValue: inputValueProp ?? (textInput ? currentSelectedItems.map(i => i.label).join(", ") : ""),
id,

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.

Action required

2. Stale textinput summary 🐞 Bug ≡ Correctness

In multi-select textInput mode, the input’s displayed summary is only seeded via
initialInputValue and updated on internal Downshift events; when value is controlled and the
parent changes value externally, there is no synchronization path to update Downshift’s
inputValue, so the input can display a stale summary that no longer matches the actual selection.
Agent Prompt
### Issue description
For multi-select `textInput`, the input shows a comma-separated summary derived from selected items. But `useCombobox` is only initialized with that summary via `initialInputValue`, and there is no effect or controlled `inputValue` prop to keep it in sync when `value` (controlled selected items) changes outside of Downshift interactions.

### Issue Context
This causes UI divergence in common flows like form reset, server-driven updates, or parent state changes: `selectedItems` in context updates (because it uses `value`), but the input continues to show the old summary.

### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[28-118]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[140-182]

### Suggested fix
Implement synchronization in `textInput` mode, for example:
- Maintain a dedicated `displayInputValue` state in the hook and pass it to `useCombobox` as `inputValue` (controlled), updating it on:
  - user typing (InputChange)
  - selection changes (when `currentSelectedItems` changes)
  - close/blur (restore summary)
- Or, if available in your Downshift hook return, call `setInputValue(summary)` in a `useEffect` when `textInput` is true and `currentSelectedItems` changes, guarded so it doesn’t overwrite active user typing (e.g., only when menu is closed / input not focused).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

1 participant