Skip to content

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

Open
rivka-ungar wants to merge 34 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 34 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 (10) 📘 Rule violations (5) 📜 Skill insights (0)

Grey Divider


Action required

1. FlexProps defined in Flex.tsx 📘 Rule violation ⚙ Maintainability
Description
The role prop was added to FlexProps inside Flex.tsx even though the component has a dedicated
Flex.types.ts file. This violates the requirement that component prop interfaces live in
*.types.ts, reducing consistency and maintainability.
Code

packages/components/layout/src/Flex/Flex.tsx[R66-69]

+  /**
+   * The ARIA role of the flex container.
+   */
+  role?: React.AriaRole;
Evidence
PR Compliance ID 2 requires component prop interfaces to be defined in a *.types.ts file. The diff
adds role?: React.AriaRole; to FlexProps in Flex.tsx, confirming the props interface is still
defined in the component file rather than Flex.types.ts.

CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps: CLAUDE.md: Component Types Must Be Defined First and Extend VibeComponentProps
packages/components/layout/src/Flex/Flex.tsx[9-70]

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

## Issue description
`FlexProps` (including the newly added `role` prop) is declared in `Flex.tsx` instead of in `Flex.types.ts`, which breaks the standard typing convention for components.
## Issue Context
The Flex component already has `Flex.types.ts`, but it currently contains only type aliases. The props interface should be moved there and exported, and `Flex.tsx` should import it.
## Fix Focus Areas
- packages/components/layout/src/Flex/Flex.tsx[9-70]
- packages/components/layout/src/Flex/Flex.types.ts[1-20]
- packages/components/layout/src/Flex/index.ts[1-1]

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


2. Flex root missing data-vibe 📘 Rule violation ◔ Observability
Description
The Flex root element still does not include a [data-vibe] attribute, even as new props (role)
are being added to the root. This blocks consistent component identification/instrumentation
expected by the compliance rule.
Code

packages/components/layout/src/Flex/Flex.tsx[R158-162]

 onMouseDown={onMouseDown}
 style={overrideStyle}
 aria-label={ariaLabel}
+        role={role}
>
Evidence
PR Compliance ID 3 requires a root [data-vibe] attribute. The Flex root element is shown with
id, data-testid, aria-*, and the newly added role, but there is no data-vibe attribute
present on the root element.

CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute: CLAUDE.md: Components Must Use forwardRef and Include a Root [data-vibe] Attribute
packages/components/layout/src/Flex/Flex.tsx[140-162]

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

## Issue description
Flex is required to render a root DOM element with a `[data-vibe]` attribute, but the root element props do not include it.
## Issue Context
The PR updates the root element props by adding `role={role}`; this is the right place to also add `data-vibe` to satisfy the required instrumentation/identification pattern.
## Fix Focus Areas
- packages/components/layout/src/Flex/Flex.tsx[140-162]

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


3. Menu callbacks inverted 🐞 Bug ≡ Correctness
Description
In useDropdownCombobox/useDropdownMultiCombobox, onIsOpenChange calls onMenuClose when isOpen is
true and onMenuOpen when isOpen is false, so consumers receive the opposite lifecycle notifications.
Code

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

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
Both combobox hooks currently invert open/close callback invocation, while the select/multi-select
hooks use the correct ordering for the same isOpen signal.

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[67-74]
packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[80-84]
packages/core/src/components/Dropdown/hooks/useDropdownSelect.ts[67-69]
packages/core/src/components/Dropdown/hooks/useDropdownMultiSelect.ts[80-82]

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

## Issue description
`onIsOpenChange` in the combobox hooks invokes `onMenuClose` when the menu opens and `onMenuOpen` when it closes.
### Issue Context
This is inconsistent with the non-combobox dropdown hooks and will cause consumer callbacks (analytics, form wiring, etc.) to fire on the wrong transition.
### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[67-74]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[80-84]
### Proposed change
- Replace `isOpen ? onMenuClose?.() : onMenuOpen?.();` with `isOpen ? onMenuOpen?.() : onMenuClose?.();` in both hooks (keep the filter-reset logic intact).

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


View more (3)
4. Random describedby id 🐞 Bug ☼ Reliability
Description
DropdownInput generates the hidden selection announcement element id using Math.random(), which is
non-deterministic across SSR and client hydration. This can cause hydration mismatches and
transiently break the aria-describedby linkage between the combobox and its description.
Code

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[R60-67]

+  // Stable id for the visually hidden element that announces the current selection.
+  // Only needed for multi-select chips; single-select already keeps the value inside the input.
+  const selectedValueId = useRef(`dropdown-selected-${Math.random().toString(36).slice(2, 9)}`).current;
+  const selectedValueText = useMemo(() => (multi ? getSelectedValueText(selectedItems) : ""), [multi, selectedItems]);
+
+  // The combobox can be described by the helper text and/or the selection announcement.
+  const describedBy =
+    [helperTextId, selectedValueText ? selectedValueId : undefined].filter(Boolean).join(" ") || undefined;
Evidence
The component currently generates a random id and uses it for aria-describedby/id, which will
not match between server and client renders. The library explicitly documents SSR support, making
deterministic markup important for hydration correctness.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[60-67]
packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[73-86]
packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[101-103]
packages/core/README.md[44-57]

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

## Issue description
`DropdownInput` creates `selectedValueId` via `Math.random()` and uses it for the visually-hidden selection announcement referenced by `aria-describedby`. In SSR/hydration flows this produces different markup server vs client, causing hydration warnings and potentially breaking the accessibility reference until React reconciles.
### Issue Context
The repo explicitly supports SSR usage for `@vibe/core`, so DOM ids must be deterministic between server and client renders.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[60-103]
- packages/core/README.md[44-57]
### Implementation notes
- Remove `Math.random()` from id creation.
- Prefer deriving the id from an existing stable identifier (e.g., the consumer-provided `id` from context): `const selectedValueId = id ? `${id}-selected-values` : undefined`.
- Only render the `<span id=...>` and include it in `aria-describedby` when `selectedValueId` is defined (or implement a deterministic counter-based id generator that is SSR-safe).

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


5. Empty inputValue overridden 🐞 Bug ≡ Correctness
Description
useDropdownCombobox sets initialInputValue using inputValueProp || selectedItem?.label, so a
controlled empty string (""), which is a valid value, is treated as falsy and replaced with the
selected label. This breaks controlled scenarios where consumers intentionally mount the input blank
even when a selection exists.
Code

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[R60-63]

+    // Seed the input with the selected item's label so a defaultValue/value is visible (and exposed to
+    // assistive technologies) on mount, now that the selection lives inside the input rather than in an overlay.
+    initialInputValue: inputValueProp || selectedItem?.label || "",
selectedItem: selectedItem,
Evidence
The single-select combobox explicitly uses || for initialInputValue, which collapses "" into
the fallback branch, while the multi-select hook uses ?? and therefore preserves an empty string.
This inconsistency demonstrates the bug and the intended correct pattern within the same codebase.

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[60-63]
packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[91-100]

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` uses `||` to derive `initialInputValue`, which incorrectly treats `""` as “unset”. With a selected item present, passing `inputValue=""` will still seed the input with `selectedItem.label`.
## Issue Context
- Controlled input value should distinguish between `undefined`/`null` and an intentional empty string.
- `useDropdownMultiCombobox` already uses `??` for this exact reason.
## Fix Focus Areas
- Change `inputValueProp || selectedItem?.label || ""` to `inputValueProp ?? selectedItem?.label ?? ""`.
### Code locations
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[60-63]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[91-100]

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


6. interactiveChips nav blocked ✓ Resolved 🐞 Bug ≡ Correctness
Description
When interactiveChips is enabled, the selection summary is written into the combobox input value,
making it non-empty whenever there are selections. DropdownInput then sets preventKeyAction=true
based on inputValue.length, which prevents the documented ArrowLeft/Backspace-from-input chip
navigation from ever being reachable after a selection exists.
Code

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[R44-47]

+  // interactiveChips: menu is always open, so isOpen would permanently suppress Backspace chip-nav.
+  // Instead suppress only when the input has text (Backspace should delete chars, not navigate chips).
+  const preventKeyAction = interactiveChips ? !!(inputValue && inputValue.length > 0) : isOpen;
+  const multipleSelectionDropdownProps = getDropdownProps ? getDropdownProps({ preventKeyAction }) : {};
Evidence
The code guarantees a non-empty inputValue in interactiveChips mode (selection summary), but
also uses inputValue.length > 0 to disable the key-action mechanism, making the documented
ArrowLeft/Backspace-from-input behavior unreachable once anything is selected.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[41-47]
packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[53-63]
packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[98-104]
packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-53]
packages/core/src/components/Dropdown/Dropdown.types.ts[20-28]

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` is documented to support chip navigation from the input (ArrowLeft/Backspace), but the current implementation writes a non-empty selection summary into the input and then derives `preventKeyAction` from `inputValue.length`, effectively disabling the key-navigation path once any item is selected.
## Issue Context
- `useDropdownMultiCombobox` writes `buildSelectionSummary(...)` into the combobox input value on mount and on every selection change when `interactiveChips` is true.
- `DropdownInput` computes `preventKeyAction` as `interactiveChips ? (inputValue.length > 0) : isOpen`, so it becomes `true` as soon as the summary is present.
- `interactiveChips` is documented to allow ArrowLeft/Backspace from the input to move focus to the last chip.
## Fix approach (one of these, pick a consistent design)
1) **Implement explicit input->chip focus handling** when `interactiveChips` is true:
- Add an input `onKeyDown` handler (or extend existing ones) that intercepts `ArrowLeft`/`Backspace` and moves focus to the last visible chip (or overflow badge) based on caret position, then `preventDefault()`.
- Do not gate this behavior on `inputValue` being empty.
2) **Change `preventKeyAction` semantics for `interactiveChips`**:
- Stop deriving `preventKeyAction` from `inputValue.length` when `interactiveChips` is enabled; derive it from a more accurate “user is actively typing a filter” signal, or keep it `false` and ensure the intended key nav still works.
3) **If the intended trade-off is ‘no typing while selection exists’**:
- Make the input effectively non-editable while showing the summary (e.g., `readOnly` in that state), and ensure ArrowLeft/Backspace still moves focus to chips.
## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[41-67]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[53-63]
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-65]
- packages/core/src/components/Dropdown/Dropdown.types.ts[20-28]

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



Remediation recommended

7. Selection text drops 0 🐞 Bug ≡ Correctness ⭐ New
Description
DropdownInput.getSelectedValueText uses truthy fallbacks (item.label || item.value || "" +
filter(Boolean)), so a valid numeric value like 0 is treated as falsy and omitted from the
selection announcement. This can cause the multi-select selection to be announced incorrectly (or as
blank) for items with an empty/missing label and value 0.
Code

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[R9-17]

+// Builds the screen-reader announcement of the current multi-select selection (the chip labels),
+// surfaced to the combobox via aria-describedby + a visually hidden element. This is how the selected
+// chips are made accessible without depending on the chip buttons themselves carrying the semantics.
+function getSelectedValueText(selectedItems: BaseItemData[]): string {
+  return selectedItems
+    .map(item => item.label || item.value || "")
+    .filter(Boolean)
+    .join(", ");
+}
Evidence
The announcement builder uses truthiness, so 0 is dropped; the data model explicitly allows
numeric values, making this a real edge case.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[9-17]
packages/core/src/components/BaseItem/BaseItem.types.ts[60-69]

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

## Issue description
`getSelectedValueText()` currently relies on truthiness (`||` + `filter(Boolean)`), which drops legitimate values like numeric `0` from the computed screen-reader announcement.

## Issue Context
- `BaseItemData.value` is `string | number`, so `0` is a valid value.
- The selection announcement is used as text content of a visually-hidden element referenced by `aria-describedby`.

## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[9-17]
- packages/core/src/components/BaseItem/BaseItem.types.ts[60-69]

## Suggested fix
- Prefer label when present, but use nullish coalescing instead of `||`, and always stringify numeric values:
 - e.g. `const text = item.label ?? String(item.value ?? "")`
 - then filter using `text.length > 0` (not `Boolean(text)`), or keep `0` explicitly.

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


8. inputRef prop ignored 🐞 Bug ≡ Correctness ⭐ New
Description
DropdownInput accepts an inputRef prop and passes it to Downshift via getInputProps, but
useDropdownCombobox overwrites any provided ref with its internal inputRef. As a result,
consumer refs never receive the input element in searchable single-select, breaking
focus/measurement integrations.
Code

packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[R142-146]

+    getInputProps: (options?: Parameters<typeof getInputProps>[0]) =>
+      getInputProps({
+        ...options,
+        ref: inputRef,
+        onKeyDown: event => {
Evidence
DropdownInput explicitly supports an external input ref, but the combobox hook’s getInputProps
wrapper overwrites ref, so the external ref cannot be attached in the single-select combobox path.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[19-54]
packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[24-26]
packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[142-146]

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

## Issue description
`DropdownInput` now supports `inputRef`, but in the searchable single-select path the hook wrapper unconditionally sets `ref: inputRef`, overriding the ref provided by the caller. This makes `inputRef` ineffective.

## Issue Context
- `DropdownInput` passes a `ref` via `getInputProps({ ref: inputRef })`.
- `useDropdownCombobox` returns a wrapped `getInputProps` that always sets `ref: inputRef`.

## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[19-54]
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[24-26]
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[142-159]

## Suggested fix
- Merge the internal and external refs instead of overwriting:
 - If `options?.ref` is a function, call it with the node.
 - If it is a ref object, assign to `.current`.
 - Always keep `inputRef.current` updated for internal focus management.
- Alternatively, change the hook API to accept an external ref and merge at the hook level.

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


9. dropdown-chip-* test IDs hardcoded 📘 Rule violation ⚙ Maintainability
Description
The updated Dropdown tests use hard-coded data-testid strings (e.g. dropdown-chip-opt1) instead
of the established test-id constants/patterns, reducing consistency and making tests more brittle to
refactors. This violates the requirement to follow repository test ID patterns from the centralized
constants.
Code

packages/core/src/components/Dropdown/tests/Dropdown.test.tsx[R755-756]

+      expect(queryByTestId("dropdown-chip-opt1")).not.toBeInTheDocument();
+      expect(getByTestId("dropdown-chip-opt3")).toBeInTheDocument();
Evidence
PR Compliance ID 7 requires tests to follow established test ID patterns from repository constants.
The updated tests assert against hard-coded ids like dropdown-chip-opt1, while the repo provides
centralized default test-id constants (including DROPDOWN and CHIP) intended for consistent test
selectors.

CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns: CLAUDE.md: Component tests must validate real behavior and accessibility attributes and follow established test ID patterns
packages/core/src/components/Dropdown/tests/Dropdown.test.tsx[755-756]
packages/core/src/tests/constants.ts[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
Dropdown tests introduced/updated in this PR assert against hard-coded `data-testid` values (e.g. `dropdown-chip-opt1`). The compliance rule requires following established test-id patterns/constants to keep tests consistent and maintainable.
## Issue Context
The repo maintains centralized test-id constants in `packages/core/src/tests/constants.ts` (e.g. `ComponentDefaultTestId.DROPDOWN`, `ComponentDefaultTestId.CHIP`). Tests and/or component test-id generation should reuse these constants (or a derived, component-specific constant built from them) instead of embedding ad-hoc strings.
## Fix Focus Areas
- packages/core/src/components/Dropdown/__tests__/Dropdown.test.tsx[755-756]
- packages/core/src/tests/constants.ts[14-37]

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


View more (7)
10. inputAriaLabel ignored 🐞 Bug ≡ Correctness
Description
DropdownInput always sets aria-labelledby when label is present, so the input’s accessible
name is forced to the label and inputAriaLabel cannot rename the inner combobox as documented.
This breaks the documented accessibility API contract for consumers needing a distinct input name.
Code

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[R75-80]

+              "aria-labelledby": label ? getLabelProps().id : undefined,
+              // When there is no visible label, the input must still have a name; fall back to the
+              // field's aria-label so the input — and the chevron that points at it — are named.
+              "aria-label": inputAriaLabel || (label ? undefined : ariaLabel),
+              "aria-describedby": describedBy,
+              // The menu is presented in a Dialog, so the combobox advertises a dialog popup.
Evidence
The component currently sets aria-labelledby whenever label exists, while also setting
aria-label from inputAriaLabel, making inputAriaLabel ineffective in the labeled case; the
docs claim inputAriaLabel can provide a name distinct from the field label.

packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[73-86]
packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[95-104]

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

## Issue description
`DropdownInput` sets both `aria-labelledby` (when `label` exists) and `aria-label` (when `inputAriaLabel` exists). Because `aria-labelledby` takes precedence for accessible-name computation, `inputAriaLabel` becomes ineffective whenever a visible label is present.
### Issue Context
The docs explicitly state `inputAriaLabel` is "useful when the input needs a name distinct from the field label".
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[73-86]
- packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[95-104]
### Proposed fix
Adjust name wiring so `inputAriaLabel` truly controls the input name:
- If `inputAriaLabel` is provided, do **not** set `aria-labelledby` on the input (or otherwise ensure the computed name uses `inputAriaLabel`).
- Otherwise, keep the current behavior: use `aria-labelledby` when `label` exists, and fall back to the field `aria-label` when it doesn’t.
Example:

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


11. Overflow ArrowLeft mismatch ✓ Resolved 🐞 Bug ≡ Correctness
Description
With interactiveChips enabled and selections overflowing, ArrowLeft from the empty input moves
focus to the +N overflow badge rather than the last visible chip, contradicting the documented
keyboard behavior. This adds an extra navigation step and diverges from the component’s stated
contract.
Code

packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[R42-53]

+        <div
+          className={styles.multiWrapper}
+          onKeyDown={e => {
+            if (
+              e.key === "ArrowLeft" &&
+              e.target instanceof HTMLInputElement &&
+              !e.target.value &&
+              overflowBadgeRef.current
+            ) {
+              overflowBadgeRef.current.focus();
+            }
+          }}
Evidence
The public prop docs state ArrowLeft from the input should focus the last chip, but the
trigger-level handler explicitly focuses the overflow badge; the input is rendered without its own
ArrowLeft handler in MultiSelectedValues, so the mismatch is real for overflow scenarios.

packages/core/src/components/Dropdown/Dropdown.types.ts[20-28]
packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-66]
packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[158-213]

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 `interactiveChips` documentation says ArrowLeft from the input moves focus to the last chip. With overflow, the current implementation focuses the `+N` badge first.
### Issue Context
- `MultiSelectTrigger` focuses `overflowBadgeRef` on ArrowLeft from an empty input.
- `MultiSelectedValues` has the necessary `itemRefs`/`visibleCount` to focus the last visible chip, but it does not handle ArrowLeft on the input wrapper.
### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[42-65]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[158-213]
- packages/core/src/components/Dropdown/Dropdown.types.ts[23-28]
### Proposed fix
Move/implement the ArrowLeft handling closer to where chip refs exist:
1. Add an `onKeyDown` handler on the `renderInput` wrapper inside `MultiSelectedValues` that:
- checks for `e.key === 'ArrowLeft'` and that the actual input is empty,
- `e.preventDefault()` and focuses `itemRefs[visibleCount - 1].current`.
2. Remove (or narrow) the `MultiSelectTrigger` ArrowLeft handler that focuses the overflow badge.
This makes the behavior match the documented contract while still allowing access to the overflow badge via normal navigation.

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


12. MultiSelectedValuesProps defined inline 📘 Rule violation ⚙ Maintainability
Description
MultiSelectedValues prop types were expanded inside the implementation file instead of being
defined in a dedicated *.types.ts file. This reduces consistency with the repo typing conventions
and makes the component API harder to discover and maintain.
Code

packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[R18-25]

disabled?: boolean;
readOnly?: boolean;
minVisibleCount?: number;
+  /** Extra props (tabIndex, onKeyDown, etc.) to spread on each visible chip container. */
+  getChipContainerProps?: (item: Item, index: number) => Record<string, any>;
+  /** Ref forwarded to the +N overflow Chips element, for external keyboard focus management. */
+  badgeRef?: React.Ref<HTMLDivElement>;
};
Evidence
PR Compliance ID 2 requires component public interfaces to be defined in *.types.ts files. The
diff shows new props being added directly to the inline MultiSelectedValuesProps type inside
MultiSelectedValues.tsx.

CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Extending VibeComponentProps: CLAUDE.md: Define Component Types First Using *.types.ts Exten...

@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

fix(Dropdown): WCAG 4.1.2 — selected value in input, interactive chips, and a11y hardening

🐞 Bug fix ✨ Enhancement 📝 Documentation 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Fixes WCAG 2.1 SC 4.1.2 (Name, Role, Value) for searchable single-select: selected label now lives
 inside the `` instead of a visual overlay, so screen readers announce the current value correctly.
• Adds interactiveChips prop for multi-select: chips are keyboard-navigable (ArrowLeft/Right,
 Backspace/Delete), selection is announced via aria-describedby on a visually-hidden element, and
 the overflow "+N" dialog manages focus on open/close.
• Hardens combobox accessibility across both modes: aria-haspopup="dialog", aria-describedby
 linking helperText, correct chevron naming via aria-labelledby/aria-label, Space-to-select
 when an option is highlighted, and focus retained on the input after selection.
• Rewrites 2 tests that asserted the old overlay behavior and adds 30+ new tests covering the new
 a11y contracts.
• Adds two new Storybook pages: "Searchable single select" and "Multi-select accessibility"
 documenting all variants and the a11y trade-offs.
Diagram

graph TD
    A["Dropdown.tsx"] --> B["DropdownComboboxController"]
    A --> C["DropdownMultiComboboxController"]
    B --> D["useDropdownCombobox"]
    C --> E["useDropdownMultiCombobox"]
    B --> F["DropdownWrapperUI"]
    C --> F
    F --> G["DropdownContext"]
    G --> H["SingleSelectTrigger"]
    G --> I["MultiSelectTrigger"]
    G --> J["TriggerActions"]
    G --> K["DropdownInput"]
    I --> L["MultiSelectedValues"]
    I --> K
    G --> M["DropdownBase"]
    M --> N["helperText span"]
    K --> O["visually-hidden span"]

    subgraph Legend
      direction LR
      _ctrl["Controller"] ~~~ _hook(["Hook"]) ~~~ _ui["UI Component"]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Write selection summary into input value (multi-select)
  • ➕ Simpler — no extra DOM element
  • ➕ Screen readers announce it as the field value, not a description
  • ➖ Blocks type-to-search while a selection exists (the input is non-empty)
  • ➖ Blocks Backspace-to-chip navigation
  • ➖ The PR already tried this approach and reverted it for these reasons
2. Use aria-live region for selection announcements
  • ➕ Decoupled from the input's aria-describedby chain
  • ➕ Announcements fire on change without focus
  • ➖ Verbose — announces on every add/remove even when user is not focused on the field
  • ➖ aria-live regions are notoriously inconsistent across screen reader / browser combinations
  • ➖ Does not satisfy 4.1.2 (value must be programmatically determinable, not just announced)

Recommendation: The PR's approach — keeping the selected label inside the native input — is the correct WCAG-compliant solution and aligns with Chakra v3/Ark UI. The main alternative worth noting is the 'visually-hidden summary in aria-describedby' pattern used for multi-select, which the PR itself iterates on (superseding the earlier approach of writing a summary into the input value). The current approach correctly separates concerns: single-select uses the input value (native semantics), multi-select uses aria-describedby (avoids blocking type-to-search).

Files changed (20) +1475 / -203

Enhancement (7) +162 / -38
DropdownInput.tsxAdd aria-haspopup, aria-describedby, visually-hidden selection announcement, and external ref/keydown +74/-27

Add aria-haspopup, aria-describedby, visually-hidden selection announcement, and external ref/keydown

• Sets 'aria-haspopup="dialog"' and 'aria-describedby' (helper text + selection announcement) on the combobox input. Adds a visually-hidden '<span>' that holds the comma-joined selected labels for multi-select, referenced via 'aria-describedby' so screen readers announce the selection without polluting the input value. Accepts an external 'inputRef' and 'onKeyDown' for the 'interactiveChips' keyboard model.

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

MultiSelectedValues.tsxAdd role=group, focus management for overflow dialog, and getChipContainerProps +58/-6

Add role=group, focus management for overflow dialog, and getChipContainerProps

• Wraps the chips container with 'role="group" aria-label="selected items"'. Adds 'getChipContainerProps' prop to spread Downshift's 'getSelectedItemProps' on each chip wrapper for keyboard navigation. Implements 'onDialogDidShow'/'onDialogDidHide' handlers to move focus into the overflow dialog on open and back to the +N badge on Esc. Fixes the overflow dialog's hide trigger (removes 'enter' to prevent immediate close on Enter keyup).

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

DropdownWrapperUI.tsxDerive and inject helperTextId into context +5/-1

Derive and inject helperTextId into context

• Computes 'helperTextId' as '${id}-helper-text' when both 'id' and 'helperText' are present, then merges it into the context value so triggers and inputs can reference it via 'aria-describedby'.

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

DropdownContext.types.tsAdd getSelectedItemProps, interactiveChips, and helperTextId to context +4/-0

Add getSelectedItemProps, interactiveChips, and helperTextId to context

• Extends 'DropdownContextProps' with 'getSelectedItemProps' (for chip keyboard navigation), 'interactiveChips' (feature flag), and 'helperTextId' (for aria-describedby wiring).

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

Dropdown.types.tsAdd interactiveChips prop to MultiSelectSpecifics +6/-0

Add interactiveChips prop to MultiSelectSpecifics

• Documents and types the new 'interactiveChips' boolean prop, restricted to multi-select mode, with a JSDoc comment explaining the keyboard navigation contract.

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

DropdownMultiComboboxController.tsxThread interactiveChips and getSelectedItemProps through the multi controller +9/-4

Thread interactiveChips and getSelectedItemProps through the multi controller

• Destructures 'interactiveChips' from props and passes it into the context value. Extracts 'getSelectedItemProps' from the hook and exposes it in context for chip keyboard navigation.

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

Flex.tsxExpose role prop on Flex component +6/-0

Expose role prop on Flex component

• Adds an optional 'role' prop to 'FlexProps' and passes it through to the underlying element, enabling 'role="group"' on the chips wrapper in 'MultiSelectedValues'.

packages/components/layout/src/Flex/Flex.tsx

Bug fix (7) +145 / -52
useDropdownCombobox.tsKeep selected label in input; add Space-to-select and focus-retention +43/-11

Keep selected label in input; add Space-to-select and focus-retention

• Removes the stateReducer overrides that forced 'inputValue' to 'null' after selection, so Downshift's default (writing the selected label into the input) is preserved. Seeds 'initialInputValue' from the selected item's label for mount-time visibility. Filters only on 'InputChange' events (not on Downshift's internal label writes). Resets the filter on menu close so reopening shows the full list. Adds Space-to-select when 'highlightedIndex >= 0', and keeps focus on the input after selection instead of blurring.

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

useDropdownMultiCombobox.tsMulti-select: Space-to-toggle, filter-on-InputChange, onOptionRemove for keyboard removal +61/-13

Multi-select: Space-to-toggle, filter-on-InputChange, onOptionRemove for keyboard removal

• Mirrors the single-select filter-on-InputChange guard and filter-reset-on-close. Adds 'FunctionSelectItem' to the stateReducer so Space-triggered selection is handled identically to Enter/click. Fires 'onOptionRemove' for keyboard-driven chip deletion (Backspace/Delete) via 'onStateChange'. Passes 'onOptionRemove' through to the hook.

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

SingleSelectTrigger.tsxRestrict selected-value overlay to non-searchable single select +8/-14

Restrict selected-value overlay to non-searchable single select

• Changes the overlay render condition from '!inputValue && selectedItem' to '!searchable && selectedItem', so the overlay only appears for non-searchable mode. Removes the 'faded' opacity style (no longer needed). Adds 'helperTextId' to the toggle button's 'aria-describedby' and fixes the chevron's 'aria-label' to never fall back to the listbox id.

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

TriggerActions.tsxFix chevron accessible name — use label/aria-label, not listbox id +14/-2

Fix chevron accessible name — use label/aria-label, not listbox id

• Computes 'chevronLabelledBy' from the visible label's id and 'chevronAriaLabel' from the field's 'aria-label'/'inputAriaLabel' string. Replaces the previous 'aria-labelledby={getMenuProps().id}' (which pointed at the listbox, not a real label) with the correct pair.

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

DropdownBase.tsxStamp helperText element with its derived id +2/-1

Stamp helperText element with its derived id

• Reads 'helperTextId' from context and applies it as the 'id' attribute on the helper text '<Text>' element, completing the 'aria-describedby' link.

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

DropdownBaseList.tsxDrop Downshift's aria-selected from multi-select items +5/-1

Drop Downshift's aria-selected from multi-select items

• Strips 'aria-selected' from Downshift's 'getItemProps' result for list items, so the 'aria-selected' set by 'BaseItem' (derived from the full 'selectedItems' array) is authoritative and correct for multi-select.

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

Trigger.module.scssRemove overlay positioning/faded styles; add visually-hidden utility class +12/-10

Remove overlay positioning/faded styles; add visually-hidden utility class

• Removes the '.hasSelected' absolute-positioning rule and the '.faded' opacity rule (both were part of the old overlay approach). Adds a '.visuallyHidden' class (standard clip-rect pattern) for the selection announcement '<span>'.

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

Tests (1) +253 / -74
Dropdown.test.tsxRewrite 2 overlay tests; add 30+ new a11y and behavior tests +253/-74

Rewrite 2 overlay tests; add 30+ new a11y and behavior tests

• Replaces the 'faded selected item' test with an assertion that the input holds the selected value, and moves the indent-stripping test to 'searchable: false'. Adds tests for: focus retention, Space-to-select (single and multi), aria-haspopup, aria-describedby for helper text, chevron naming, aria-selected on options, chips group role, labelled remove buttons, and the full 'interactiveChips' selection-announcement contract.

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

Documentation (4) +830 / -0
DropdownSearchableSingleSelect.stories.tsxAdd comprehensive Storybook stories for searchable single select +564/-0

Add comprehensive Storybook stories for searchable single select

• New story file with 11 stories covering: overview, sizes, states, default/controlled value, icons & avatars, end elements, valueRenderer, groups (sticky/divider), tooltips, clearable/max-height, and custom filter/empty message.

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

DropdownSearchableSingleSelect.mdxAdd MDX documentation page for searchable single select accessibility +123/-0

Add MDX documentation page for searchable single select accessibility

• Documents the behavioral change (selected value in input vs overlay), the WCAG rationale, what happens on open/reopen/type, the text-only trade-off for collapsed selected values, and a reference table of all accessibility-relevant props.

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

DropdownMultiSelectA11y.stories.tsxAdd Storybook stories for multi-select interactiveChips +80/-0

Add Storybook stories for multi-select interactiveChips

• Two stories: 'InteractiveChipsBasic' (basic keyboard-navigable chips) and 'InteractiveChipsOverflow' (chips with 'minVisibleCount' and the +N overflow dialog).

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

DropdownMultiSelectA11y.mdxAdd MDX documentation page for multi-select accessibility and interactiveChips +63/-0

Add MDX documentation page for multi-select accessibility and interactiveChips

• Documents the 'interactiveChips' keyboard model, the selection-announcement via 'aria-describedby', the Space-selects behavior, the chevron label fix, and the chips group role. Includes a changelog of the 2026-06-25 and 2026-06-30 iterations and the trade-offs of each approach.

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

Other (1) +85 / -39
MultiSelectTrigger.tsxAdd interactiveChips rendering path with keyboard chip navigation +85/-39

Add interactiveChips rendering path with keyboard chip navigation

• Extracts trigger content into 'renderTriggerContent()' with a dedicated branch for 'interactiveChips && searchable'. In that branch, chips are always visible alongside the input, 'getChipContainerProps' wires Downshift's 'getSelectedItemProps' for keyboard focus management, and ArrowLeft from an empty input moves focus to the overflow badge. Fixes the chevron's 'aria-label' and adds 'aria-describedby' for helper text.

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

@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.07KB -185B 🟢
@vibe/icon 12.93KB 12.87KB -59B 🟢
@vibe/layer 2.97KB 2.96KB -10B 🟢
@vibe/layout 9.86KB 9.82KB -37B 🟢
@vibe/loader 5.67KB 5.65KB -28B 🟢
@vibe/tooltip 61.34KB 61.31KB -31B 🟢
@vibe/typography 63.45KB 63.39KB -68B 🟢
Accordion 6.32KB 6.3KB -23B 🟢
AccordionItem 66.48KB 66.49KB +11B 🔺
AlertBanner 70.87KB 70.86KB -8B 🟢
AlertBannerButton 18.82KB 18.78KB -32B 🟢
AlertBannerLink 15.25KB 15.23KB -30B 🟢
AlertBannerText 63.98KB 63.92KB -68B 🟢
AttentionBox 74.34KB 74.35KB +11B 🔺
Avatar 66.77KB 66.75KB -18B 🟢
AvatarGroup 93.24KB 93.32KB +83B 🔺
Badge 43.19KB 43.22KB +30B 🔺
BreadcrumbItem 64.69KB 64.73KB +46B 🔺
BreadcrumbMenu 68.6KB 68.58KB -22B 🟢
BreadcrumbMenuItem 77.16KB 77.07KB -88B 🟢
BreadcrumbsBar 5.68KB 5.68KB -3B 🟢
ButtonGroup 68.34KB 68.35KB +12B 🔺
Checkbox 66.92KB 66.87KB -54B 🟢
Chips 75.16KB 75.06KB -98B 🟢
ColorPicker 74.46KB 74.48KB +29B 🔺
ColorPickerContent 73.86KB 73.74KB -117B 🟢
Combobox 84.11KB 84.15KB +40B 🔺
Counter 42.2KB 42.25KB +54B 🔺
DatePicker 112.62KB 113.42KB +817B 🔺
Divider 5.43KB 5.43KB -2B 🟢
Dropdown 95.49KB 96.23KB +754B 🔺
EditableHeading 66.63KB 66.62KB -12B 🟢
EditableText 66.53KB 66.46KB -76B 🟢
EmptyState 70.49KB 70.44KB -46B 🟢
ExpandCollapse 66.3KB 66.26KB -41B 🟢
FormattedNumber 5.82KB 5.83KB +15B 🔺
GridKeyboardNavigationContext 4.66KB 4.65KB -16B 🟢
HiddenText 5.42KB 5.39KB -35B 🟢
Info 72.12KB 72.07KB -50B 🟢
Label 68.62KB 68.71KB +96B 🔺
Link 14.86KB 14.92KB +62B 🔺
List 72.83KB 72.96KB +132B 🔺
ListItem 65.54KB 65.49KB -46B 🟢
ListItemAvatar 66.96KB 66.93KB -31B 🟢
ListItemIcon 13.96KB 14KB +35B 🔺
ListTitle 65.02KB 65.06KB +40B 🔺
Menu 8.64KB 8.62KB -19B 🟢
MenuDivider 5.56KB 5.55KB -3B 🟢
MenuGridItem 7.21KB 7.19KB -12B 🟢
MenuItem 76.96KB 76.93KB -32B 🟢
MenuItemButton 70.1KB 70.09KB -3B 🟢
MenuTitle 65.37KB 65.35KB -17B 🟢
MenuButton 66.1KB 66.15KB +50B 🔺
Modal 79.17KB 79.18KB +8B 🔺
ModalContent 4.73KB 4.71KB -12B 🟢
ModalHeader 65.83KB 65.83KB -4B 🟢
ModalMedia 7.52KB 7.48KB -40B 🟢
ModalFooter 67.69KB 67.7KB +12B 🔺
ModalFooterWizard 68.6KB 68.62KB +24B 🔺
ModalBasicLayout 8.94KB 8.93KB -11B 🟢
ModalMediaLayout 8.1KB 8.06KB -35B 🟢
ModalSideBySideLayout 6.3KB 6.29KB -9B 🟢
MultiStepIndicator 52.93KB 52.93KB +3B 🔺
NumberField 72.85KB 72.83KB -24B 🟢
ProgressBar 7.37KB 7.34KB -29B 🟢
RadioButton 65.92KB 65.92KB -5B 🟢
Search 70.64KB 70.7KB +53B 🔺
Skeleton 5.99KB 5.98KB -5B 🟢
Slider 73.99KB 73.86KB -141B 🟢
SplitButton 66.51KB 66.46KB -59B 🟢
SplitButtonMenu 8.8KB 8.77KB -30B 🟢
Steps 71.35KB 71.3KB -50B 🟢
Table 7.29KB 7.25KB -43B 🟢
TableBody 66.79KB 66.75KB -38B 🟢
TableCell 65.18KB 65.27KB +95B 🔺
TableContainer 5.34KB 5.32KB -24B 🟢
TableHeader 5.64KB 5.64KB -4B 🟢
TableHeaderCell 72.22KB 72.21KB -12B 🟢
TableRow 5.57KB 5.55KB -27B 🟢
TableRowMenu 68.87KB 68.82KB -45B 🟢
TableVirtualizedBody 71.35KB 71.51KB +160B 🔺
Tab 64.08KB 64.06KB -20B 🟢
TabList 8.89KB 8.9KB +9B 🔺
TabPanel 5.31KB 5.29KB -15B 🟢
TabPanels 5.85KB 5.84KB -18B 🟢
TabsContext 5.47KB 5.51KB +33B 🔺
TextArea 66.37KB 66.35KB -25B 🟢
TextField 69.48KB 69.41KB -71B 🟢
TextWithHighlight 64.29KB 64.36KB +72B 🔺
ThemeProvider 4.37KB 4.36KB -8B 🟢
Tipseen 71.24KB 71.16KB -84B 🟢
TipseenContent 71.65KB 71.63KB -25B 🟢
TipseenMedia 71.42KB 71.4KB -22B 🟢
TipseenWizard 73.88KB 73.89KB +9B 🔺
Toast 74.1KB 74.09KB -9B 🟢
ToastButton 18.64KB 18.61KB -33B 🟢
ToastLink 15.12KB 15.12KB +1B 🔺
Toggle 66.76KB 66.63KB -136B 🟢
TransitionView 5.46KB 5.43KB -26B 🟢
VirtualizedGrid 12.52KB 12.54KB +23B 🔺
VirtualizedList 12.3KB 12.27KB -29B 🟢
List (Next) 8.19KB 8.16KB -26B 🟢
ListItem (Next) 69.95KB 69.95KB -8B 🟢
ListTitle (Next) 65.37KB 65.41KB +41B 🔺

📊 Summary:

  • Total Base Size: 4.76MB
  • Total PR Size: 4.76MB
  • Total Difference: +197B

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();
}
}}

This comment was marked as resolved.

…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>
Comment on lines +66 to +69
/**
* The ARIA role of the flex container.
*/
role?: React.AriaRole;

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. flexprops not in .types.ts 📘 Rule violation ⚙ Maintainability

FlexProps is still declared in Flex.tsx even though a Flex.types.ts file exists, and this PR
adds another prop (role) there. This breaks the required typing convention and makes the component
harder to maintain consistently.
Agent Prompt
## Issue description
`FlexProps` is declared in `Flex.tsx` instead of the dedicated `Flex.types.ts` file, and this PR extends that in-file interface by adding `role`.

## Issue Context
The compliance rule requires component prop types to live in `*.types.ts` (and extend `VibeComponentProps`) to keep a predictable, maintainable structure.

## Fix Focus Areas
- packages/components/layout/src/Flex/Flex.tsx[9-70]
- packages/components/layout/src/Flex/Flex.types.ts[1-20]

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

Comment thread packages/components/layout/src/Flex/Flex.tsx
Comment on lines 67 to 74
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?.();
},

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

4. Menu callbacks inverted 🐞 Bug ≡ Correctness

In both useDropdownCombobox and useDropdownMultiCombobox, onIsOpenChange calls onMenuClose when
isOpen becomes true and onMenuOpen when it becomes false, so consumers observe swapped open/close
events.
Agent Prompt
## Issue description
Searchable Dropdown hooks invert `onMenuOpen`/`onMenuClose` callbacks in `onIsOpenChange`, causing consumers to receive the wrong lifecycle events.

## Issue Context
Non-searchable hooks use `isOpen ? onMenuOpen : onMenuClose`, but the combobox-based hooks currently do the opposite.

## Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[67-74]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[100-104]
- packages/core/src/components/Dropdown/hooks/useDropdownSelect.ts[67-69]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiSelect.ts[80-82]

## Suggested fix
Change both combobox hooks to:
- `if (isOpen) onMenuOpen?.(); else onMenuClose?.();`

Keep the filter-reset logic as-is; only fix the callback direction.

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

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

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 3f226c9

Each chip in the selected-items group is now a single button with
aria-label "Remove <item>", instead of a labelled div wrapping a separate
× button. Clicking (or Enter/Space) removes the chip; downshift keyboard
nav props still merge onto the same button for interactiveChips.

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

qodo-free-for-open-source-projects Bot commented Jun 25, 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

…ummary

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

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 2a405f8

downshift's useCombobox sets aria-selected to mark a single tracked
selectedItem, so in multi-select the chosen options were always
aria-selected="false". Strip downshift's aria-selected in DropdownBaseList so
BaseItem's value — derived from the full selectedItems list — is authoritative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +44 to +47
// interactiveChips: menu is always open, so isOpen would permanently suppress Backspace chip-nav.
// Instead suppress only when the input has text (Backspace should delete chars, not navigate chips).
const preventKeyAction = interactiveChips ? !!(inputValue && inputValue.length > 0) : isOpen;
const multipleSelectionDropdownProps = getDropdownProps ? getDropdownProps({ preventKeyAction }) : {};

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. Interactivechips nav blocked 🐞 Bug ≡ Correctness

When interactiveChips is enabled, the selection summary is written into the combobox input value,
making it non-empty whenever there are selections. DropdownInput then sets preventKeyAction=true
based on inputValue.length, which prevents the documented ArrowLeft/Backspace-from-input chip
navigation from ever being reachable after a selection exists.
Agent Prompt
## Issue description
`interactiveChips` is documented to support chip navigation from the input (ArrowLeft/Backspace), but the current implementation writes a non-empty selection summary into the input and then derives `preventKeyAction` from `inputValue.length`, effectively disabling the key-navigation path once any item is selected.

## Issue Context
- `useDropdownMultiCombobox` writes `buildSelectionSummary(...)` into the combobox input value on mount and on every selection change when `interactiveChips` is true.
- `DropdownInput` computes `preventKeyAction` as `interactiveChips ? (inputValue.length > 0) : isOpen`, so it becomes `true` as soon as the summary is present.
- `interactiveChips` is documented to allow ArrowLeft/Backspace from the input to move focus to the last chip.

## Fix approach (one of these, pick a consistent design)
1) **Implement explicit input->chip focus handling** when `interactiveChips` is true:
   - Add an input `onKeyDown` handler (or extend existing ones) that intercepts `ArrowLeft`/`Backspace` and moves focus to the last visible chip (or overflow badge) based on caret position, then `preventDefault()`.
   - Do not gate this behavior on `inputValue` being empty.

2) **Change `preventKeyAction` semantics for `interactiveChips`**:
   - Stop deriving `preventKeyAction` from `inputValue.length` when `interactiveChips` is enabled; derive it from a more accurate “user is actively typing a filter” signal, or keep it `false` and ensure the intended key nav still works.

3) **If the intended trade-off is ‘no typing while selection exists’**:
   - Make the input effectively non-editable while showing the summary (e.g., `readOnly` in that state), and ensure ArrowLeft/Backspace still moves focus to chips.

## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[41-67]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[53-63]
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-65]
- packages/core/src/components/Dropdown/Dropdown.types.ts[20-28]

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

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

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 25d217d

The expand/collapse chevron is focusable and needs its own accessible
name (WCAG 4.1.2). It was labelled by the menu, naming it after the
option list. Point it at the visible label instead, falling back to the
combobox input (searchable) or toggle button (non-searchable) when there
is no label. Also fix the input/trigger no-label aria-label fallback,
which set an element id string rather than the field's real aria-label.

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

Copy link
Copy Markdown
Contributor

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

… summary-in-value

Replace the inaccessible chips approach (selection summary stuffed into the
combobox value + each chip as a single "Remove <item>" button in a role=group)
with the visually-hidden + aria-describedby announcement from PR #3351, reading
out the selected values. Revert the chips to a chip + labelled × remove button.

Keeping the selection out of the input value restores type-to-search and
Backspace-to-chip removal, which the value summary had disabled. Update tests
and the multi-select a11y doc (new dated section above the preserved history).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +60 to +67
// Stable id for the visually hidden element that announces the current selection.
// Only needed for multi-select chips; single-select already keeps the value inside the input.
const selectedValueId = useRef(`dropdown-selected-${Math.random().toString(36).slice(2, 9)}`).current;
const selectedValueText = useMemo(() => (multi ? getSelectedValueText(selectedItems) : ""), [multi, selectedItems]);

// The combobox can be described by the helper text and/or the selection announcement.
const describedBy =
[helperTextId, selectedValueText ? selectedValueId : undefined].filter(Boolean).join(" ") || undefined;

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. Random describedby id 🐞 Bug ☼ Reliability

DropdownInput generates the hidden selection announcement element id using Math.random(), which is
non-deterministic across SSR and client hydration. This can cause hydration mismatches and
transiently break the aria-describedby linkage between the combobox and its description.
Agent Prompt
### Issue description
`DropdownInput` creates `selectedValueId` via `Math.random()` and uses it for the visually-hidden selection announcement referenced by `aria-describedby`. In SSR/hydration flows this produces different markup server vs client, causing hydration warnings and potentially breaking the accessibility reference until React reconciles.

### Issue Context
The repo explicitly supports SSR usage for `@vibe/core`, so DOM ids must be deterministic between server and client renders.

### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[60-103]
- packages/core/README.md[44-57]

### Implementation notes
- Remove `Math.random()` from id creation.
- Prefer deriving the id from an existing stable identifier (e.g., the consumer-provided `id` from context): `const selectedValueId = id ? `${id}-selected-values` : undefined`.
- Only render the `<span id=...>` and include it in `aria-describedby` when `selectedValueId` is defined (or implement a deterministic counter-based id generator that is SSR-safe).

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

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

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 8b7517d

Wrap the multi-select chips in role="group" with aria-label "selected items"
so assistive tech presents them as a single named set. Add a test and a new
dated section to the multi-select a11y doc.

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

Copy link
Copy Markdown
Contributor

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Test / Test

Failed stage: Run Test [❌]

Failed test name: DatePicker tests > snapshot tests > renders correctly

Failure summary:

The action failed because the test suite exited with code 1 due to a snapshot mismatch in the
DatePicker tests.
- Failing file/test: src/components/DatePicker/tests/DatePicker.test.tsx
DatePicker tests > snapshot tests > renders correctly
- Error: Snapshot DatePicker tests > snapshot
tests > renders correctly 1 mismatched.
- Snapshot diff indicates an attribute change on the
rendered element:
aria-labelledby changed from downshift-1-menu (expected) to
downshift-1-toggle-button (received).
- The failure is reported at
src/components/DatePicker/tests/DatePicker.test.tsx:19:25 where
expect(container).toMatchSnapshot() is called.
As a result, @vibe/core:test failed and Lerna/Nx
reported Running target test for 5 projects failed, causing the GitHub Action to fail.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

180:  * [new branch]          feature/shanab/input-type-time -> origin/feature/shanab/input-type-time
181:  * [new branch]          fix-add-tslib               -> origin/fix-add-tslib
182:  * [new branch]          fix-chromatic-action        -> origin/fix-chromatic-action
183:  * [new branch]          fix-dialog-show-contextmenu-default-menu -> origin/fix-dialog-show-contextmenu-default-menu
184:  * [new branch]          fix-dropdown-spec           -> origin/fix-dropdown-spec
185:  * [new branch]          fix-showHideEvent-in-dialog-story -> origin/fix-showHideEvent-in-dialog-story
186:  * [new branch]          fix/a11y-aria-label-and-focus-rings -> origin/fix/a11y-aria-label-and-focus-rings
187:  * [new branch]          fix/add-missing-style-dependency -> origin/fix/add-missing-style-dependency
188:  * [new branch]          fix/bundle-size-check-exclude-peers -> origin/fix/bundle-size-check-exclude-peers
189:  * [new branch]          fix/combobox-spacings-9062326510 -> origin/fix/combobox-spacings-9062326510
190:  * [new branch]          fix/modal-focus             -> origin/fix/modal-focus
191:  * [new branch]          fix/orhal/playwright-install-performance-fix -> origin/fix/orhal/playwright-install-performance-fix
192:  * [new branch]          fix/single-searchable-dropdown-a11y-selected-value -> origin/fix/single-searchable-dropdown-a11y-selected-value
193:  * [new branch]          fix/yossi/test-focus-lock-esm -> origin/fix/yossi/test-focus-lock-esm
194:  * [new branch]          gh-pages                    -> origin/gh-pages
195:  * [new branch]          lint-error-fixes            -> origin/lint-error-fixes
196:  * [new branch]          master                      -> origin/master
...

1879:  Extracting ...
1880:  [command]/usr/bin/tar xz --strip 1 --warning=no-unknown-keyword --overwrite -C /home/runner/work/_temp/6b691893-eb46-491b-9831-50d97ce6aed5 -f /home/runner/work/_temp/9afc9f56-0d3e-4077-a07a-f7f79e2eec3c
1881:  Adding to the cache ...
1882:  ##[group]Environment details
1883:  node: v20.12.2
1884:  npm: 10.5.0
1885:  yarn: 1.22.22
1886:  ##[endgroup]
1887:  [command]/usr/local/bin/yarn --version
1888:  1.22.22
1889:  [command]/usr/local/bin/yarn cache dir
1890:  /home/runner/.cache/yarn/v6
1891:  [command]/usr/local/bin/yarn config get enableGlobalCache
1892:  undefined
1893:  Cache hit for: node-cache-Linux-x64-yarn-f777e8b84823d80303995aaaabc137757d5fa4dcd22f4326b09d8be0af02ab30
1894:  (node:2290) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
1895:  Received 264241152 of 1328278344 (19.9%), 252.0 MBs/sec
...

2414:  �[2K�[1G�[2m$ node scripts/generate-lazy-icons.js�[22m
2415:  Generated lazy components and index.ts for 276 icons.
2416:  �[2K�[1G�[2m$ node scripts/generate-svg-index.js�[22m
2417:  Generated index.ts for SVG exports in ./src/svg
2418:  �[36m
2419:  �[1msrc/react/index.ts, src/lazy/index.ts, src/svg/index.ts, src/iconsMetaData.ts, src/types.ts�[22m → �[1mdist�[22m...�[39m
2420:  �[1m�[33m(!) Generated an empty chunk�[39m�[22m
2421:  types
2422:  �[32mcreated �[1mdist�[22m in �[1m44.6s�[22m�[39m
2423:  �[2K�[1GDone in 53.29s.
2424:  ##[endgroup]
2425:  Lerna (powered by Nx)   Successfully ran target build for 4 projects
2426:  Done in 90.65s.
2427:  ##[group]Run if [[ -n "$(git status --porcelain yarn.lock)" ]]; then
2428:  �[36;1mif [[ -n "$(git status --porcelain yarn.lock)" ]]; then�[0m
2429:  �[36;1m  echo "Error: yarn.lock has uncommitted changes. Please commit it."�[0m
2430:  �[36;1m  exit 1�[0m
...

2674:  �[32m✓�[39m src/components/Slider/__tests__/Slider-non-ranged.test.tsx �[2m (�[22m�[2m10 tests�[22m�[2m)�[22m�[33m 512�[2mms�[22m�[39m
2675:  �[32m✓�[39m src/components/Menu/MenuItem/components/BaseMenuItem/__tests__/BaseMenuItem.test.tsx �[2m (�[22m�[2m7 tests�[22m�[2m)�[22m�[90m 76�[2mms�[22m�[39m
2676:  �[32m✓�[39m src/components/Menu/MenuGridItem/__tests__/useMenuGridItemNavContext.test.ts �[2m (�[22m�[2m5 tests�[22m�[2m)�[22m�[90m 33�[2mms�[22m�[39m
2677:  �[32m✓�[39m src/components/Menu/Menu/__tests__/Menu.test.tsx �[2m (�[22m�[2m7 tests�[22m �[2m|�[22m �[33m4 skipped�[39m�[2m)�[22m�[90m 51�[2mms�[22m�[39m
2678:  �[90mstderr�[2m | src/components/Menu/Menu/__tests__/Menu.test.tsx�[2m > �[22m�[2mSnapshots�[2m > �[22m�[2mrenders correctly with children�[22m�[39m
2679:  Menu child must be a menuChild item (such as MenuItem, MenuDivider, MenuTitle, etc). This child is not supported:  {
2680:  '$$typeof': Symbol(react.element),
2681:  type: [Function: Divider],
2682:  key: '.2',
2683:  ref: null,
2684:  props: {},
2685:  _owner: null,
2686:  _store: {}
2687:  }
2688:  �[32m✓�[39m src/components/EditableTypography/__tests__/EditableTypography.test.tsx �[2m (�[22m�[2m4 tests�[22m�[2m)�[22m�[90m 245�[2mms�[22m�[39m
2689:  �[33m❯�[39m src/components/DatePicker/__tests__/DatePicker.test.tsx �[2m (�[22m�[2m8 tests�[22m �[2m|�[22m �[31m1 failed�[39m�[2m)�[22m�[33m 4809�[2mms�[22m�[39m
2690:  �[31m   �[33m❯�[31m src/components/DatePicker/__tests__/DatePicker.test.tsx�[2m > �[22mDatePicker tests�[2m > �[22msnapshot tests�[2m > �[22mrenders correctly�[39m
...

2802:  �[32m✓�[39m src/components/Toast/__tests__/Toast.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 67�[2mms�[22m�[39m
2803:  �[32m✓�[39m src/components/Tabs/TabPanels/__tests__/TabPanels.snapshot.test.tsx �[2m (�[22m�[2m4 tests�[22m�[2m)�[22m�[90m 24�[2mms�[22m�[39m
2804:  �[32m✓�[39m src/components/Menu/MenuItem/components/MenuItemSubMenuIcon/__tests__/MenuItemSubMenuIcon.test.tsx �[2m (�[22m�[2m4 tests�[22m�[2m)�[22m�[90m 143�[2mms�[22m�[39m
2805:  �[32m✓�[39m src/components/BreadcrumbsBar/__tests__/BreadcrumbsBar.snapshot.test.tsx �[2m (�[22m�[2m4 tests�[22m�[2m)�[22m�[90m 41�[2mms�[22m�[39m
2806:  �[32m✓�[39m src/components/Menu/Menu/utils/__tests__/utils.test.ts �[2m (�[22m�[2m5 tests�[22m�[2m)�[22m�[90m 5�[2mms�[22m�[39m
2807:  �[32m✓�[39m src/components/ButtonGroup/__tests__/ButtonGroup.snapshot.test.tsx �[2m (�[22m�[2m7 tests�[22m�[2m)�[22m�[90m 42�[2mms�[22m�[39m
2808:  �[32m✓�[39m src/components/Menu/MenuItem/__tests__/MenuItem.snapshot.test.tsx �[2m (�[22m�[2m7 tests�[22m�[2m)�[22m�[90m 57�[2mms�[22m�[39m
2809:  �[32m✓�[39m src/components/SplitButton/SplitButtonMenu/__tests__/SplitButtonMenu.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 109�[2mms�[22m�[39m
2810:  �[32m✓�[39m src/components/Label/__tests__/Label.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 107�[2mms�[22m�[39m
2811:  �[32m✓�[39m src/components/Link/__tests__/Link.snapshot.test.tsx �[2m (�[22m�[2m8 tests�[22m�[2m)�[22m�[90m 28�[2mms�[22m�[39m
2812:  �[32m✓�[39m src/components/Table/Table/__tests__/TableHelpers.test.tsx �[2m (�[22m�[2m9 tests�[22m�[2m)�[22m�[90m 5�[2mms�[22m�[39m
2813:  �[32m✓�[39m src/components/Badge/__tests__/Badge.snapshot.test.tsx �[2m (�[22m�[2m6 tests�[22m�[2m)�[22m�[90m 44�[2mms�[22m�[39m
2814:  �[32m✓�[39m src/components/AttentionBox/layouts/AttentionBoxCompact/__tests__/AttentionBoxCompact.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 175�[2mms�[22m�[39m
2815:  �[32m✓�[39m src/components/Counter/__tests__/Counter.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 56�[2mms�[22m�[39m
2816:  �[32m✓�[39m src/hooks/useAfterFirstRender/__tests__/useAfterFirstRender.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 27�[2mms�[22m�[39m
2817:  error Command failed with exit code 1.
2818:  �[32m✓�[39m src/components/Info/components/InfoLink/__tests__/InfoLink.test.tsx �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 96�[2mms�[22m�[39m
2819:  �[90mstderr�[2m | src/components/Menu/MenuGridItem/__tests__/MenuGridItem.snapshot.test.tsx�[2m > �[22m�[2mMenuGridItem renders correctly�[2m > �[22m�[2mwith a disabled child�[22m�[39m
2820:  Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
2821:  Check the render method of `ForwardRef`.
...

2830:  �[32m✓�[39m src/components/Avatar/__tests__/Avatar.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 50�[2mms�[22m�[39m
2831:  �[32m✓�[39m src/components/Menu/MenuGridItem/__tests__/MenuGridItem.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 32�[2mms�[22m�[39m
2832:  �[32m✓�[39m src/components/SplitButton/SplitButtonMenu/__tests__/SplitButtonMenu.snapshot.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 33�[2mms�[22m�[39m
2833:  �[32m✓�[39m src/components/VirtualizedGrid/__tests__/VirtualizedGrid.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 18�[2mms�[22m�[39m
2834:  �[32m✓�[39m src/components/Accordion/AccordionItem/__tests__/AccordionItem.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 108�[2mms�[22m�[39m
2835:  �[32m✓�[39m src/components/Menu/MenuTitle/__tests__/MenuTitle.test.tsx �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 20�[2mms�[22m�[39m
2836:  �[32m✓�[39m src/components/HiddenText/__tests__/HiddenText.test.tsx �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 32�[2mms�[22m�[39m
2837:  �[32m✓�[39m src/components/Badge/Indicator/__tests__/Indicator.snapshot.test.tsx �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 12�[2mms�[22m�[39m
2838:  �[32m✓�[39m src/components/ListItemIcon/__tests__/ListItemIcon.test.tsx �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 14�[2mms�[22m�[39m
2839:  �[32m✓�[39m src/__tests__/exports.test.ts �[2m (�[22m�[2m3 tests�[22m�[2m)�[22m�[90m 6�[2mms�[22m�[39m
2840:  �[32m✓�[39m src/components/ListItemAvatar/__tests__/ListItemAvatar.test.tsx �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 29�[2mms�[22m�[39m
2841:  �[32m✓�[39m src/components/Divider/__tests__/Divider.test.tsx �[2m (�[22m�[2m2 tests�[22m�[2m)�[22m�[90m 16�[2mms�[22m�[39m
2842:  �[32m✓�[39m src/components/ListTitle/__tests__/ListTitle.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 15�[2mms�[22m�[39m
2843:  �[32m✓�[39m src/components/TextWithHighlight/__tests__/TextWithHighlight.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 13�[2mms�[22m�[39m
2844:  �[32m✓�[39m src/components/VirtualizedList/__tests__/VirtualizedList.snapshot.test.tsx �[2m (�[22m�[2m1 test�[22m�[2m)�[22m�[90m 12�[2mms�[22m�[39m
2845:  �[31m⎯⎯⎯⎯⎯⎯⎯�[1m�[7m Failed Tests 1 �[27m�[22m⎯⎯⎯⎯⎯⎯⎯�[39m
2846:  �[31m�[1m�[7m FAIL �[27m�[22m�[39m src/components/DatePicker/__tests__/DatePicker.test.tsx�[2m > �[22mDatePicker tests�[2m > �[22msnapshot tests�[2m > �[22mrenders correctly
2847:  �[31m�[1mError�[22m: Snapshot `DatePicker tests > snapshot tests > renders correctly 1` mismatched�[39m
2848:  - Expected
...

2869:  -                             aria-labelledby="downshift-1-menu"
2870:  +                             aria-labelledby="downshift-1-toggle-button"
2871:  class="button sizeMedium kindTertiary colorPrimary noSidePadding"
2872:  data-testid="icon-button"
2873:  data-vibe="Button"
2874:  style="justify-content: center; align-items: center; padding: 0px; width: 24px; height: 24px;"
2875:  tabindex="-1"
2876:  �[36m �[2m❯�[22m src/components/DatePicker/__tests__/DatePicker.test.tsx:�[2m19:25�[22m�[39m
2877:  �[90m 17| �[39m    �[34mit�[39m(�[32m"renders correctly"�[39m�[33m,�[39m () �[33m=>�[39m {
2878:  �[90m 18| �[39m      �[35mconst�[39m { container } �[33m=�[39m �[34mrenderDatePicker�[39m({ date�[33m:�[39m �[35mnew�[39m �[33mDate�[39m(�[34m1992�[39m�[33m,�[39m �[34m2�[39m�[33m,�[39m…
2879:  �[90m 19| �[39m      �[34mexpect�[39m(container)�[33m.�[39m�[34mtoMatchSnapshot�[39m()�[33m;�[39m
2880:  �[90m   | �[39m                        �[31m^�[39m
2881:  �[90m 20| �[39m    })�[33m;�[39m
2882:  �[90m 21| �[39m  })�[33m;�[39m
2883:  �[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m
2884:  ##[error]Error: Snapshot `DatePicker tests > snapshot tests > renders correctly 1` mismatched
2885:  
...

2904:                                aria-busy="false"
2905:                                aria-controls="downshift-1-menu"
2906:                                aria-disabled="false"
2907:                                aria-expanded="false"
2908:  -                             aria-labelledby="downshift-1-menu"
2909:  +                             aria-labelledby="downshift-1-toggle-button"
2910:                                class="button sizeMedium kindTertiary colorPrimary noSidePadding"
2911:                                data-testid="icon-button"
2912:                                data-vibe="Button"
2913:                                style="justify-content: center; align-items: center; padding: 0px; width: 24px; height: 24px;"
2914:                                tabindex="-1"
2915:  
2916:   ❯ src/components/DatePicker/__tests__/DatePicker.test.tsx:19:25
2917:  
2918:  
2919:  �[2m  Snapshots �[22m �[1m�[31m1 failed�[39m�[22m
2920:  �[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m142 passed�[39m�[22m�[90m (143)�[39m
2921:  �[2m      Tests �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m1375 passed�[39m�[22m�[2m | �[22m�[33m7 skipped�[39m�[90m (1383)�[39m
2922:  �[2m   Start at �[22m 11:34:38
2923:  �[2m   Duration �[22m 78.94s�[2m (transform 9.82s, setup 26.13s, collect 74.89s, tests 31.17s, environment 65.46s, prepare 14.24s)�[22m
2924:  �[2K�[1G�[31merror�[39m Command failed with exit code 1.
2925:  �[2K�[1G�[34minfo�[39m Visit �[1mhttps://yarnpkg.com/en/docs/cli/run�[22m for documentation about this command.
2926:  ##[endgroup]
2927:  Lerna (powered by Nx)   Running target test for 5 projects failed
2928:  Failed tasks:
2929:  - @vibe/core:test
2930:  info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
2931:  ##[error]Process completed with exit code 1.
2932:  Post job cleanup.

Comment on lines +66 to +69
/**
* The ARIA role of the flex container.
*/
role?: React.AriaRole;

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. flexprops defined in flex.tsx 📘 Rule violation ⚙ Maintainability

The role prop was added to FlexProps inside Flex.tsx even though the component has a dedicated
Flex.types.ts file. This violates the requirement that component prop interfaces live in
*.types.ts, reducing consistency and maintainability.
Agent Prompt
## Issue description
`FlexProps` (including the newly added `role` prop) is declared in `Flex.tsx` instead of in `Flex.types.ts`, which breaks the standard typing convention for components.

## Issue Context
The Flex component already has `Flex.types.ts`, but it currently contains only type aliases. The props interface should be moved there and exported, and `Flex.tsx` should import it.

## Fix Focus Areas
- packages/components/layout/src/Flex/Flex.tsx[9-70]
- packages/components/layout/src/Flex/Flex.types.ts[1-20]
- packages/components/layout/src/Flex/index.ts[1-1]

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

Comment on lines 158 to 162
onMouseDown={onMouseDown}
style={overrideStyle}
aria-label={ariaLabel}
role={role}
>

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. flex root missing data-vibe 📘 Rule violation ◔ Observability

The Flex root element still does not include a [data-vibe] attribute, even as new props (role)
are being added to the root. This blocks consistent component identification/instrumentation
expected by the compliance rule.
Agent Prompt
## Issue description
Flex is required to render a root DOM element with a `[data-vibe]` attribute, but the root element props do not include it.

## Issue Context
The PR updates the root element props by adding `role={role}`; this is the right place to also add `data-vibe` to satisfy the required instrumentation/identification pattern.

## Fix Focus Areas
- packages/components/layout/src/Flex/Flex.tsx[140-162]

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

Comment on lines 67 to 74
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?.();
},

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. Menu callbacks inverted 🐞 Bug ≡ Correctness

In useDropdownCombobox/useDropdownMultiCombobox, onIsOpenChange calls onMenuClose when isOpen is
true and onMenuOpen when isOpen is false, so consumers receive the opposite lifecycle notifications.
Agent Prompt
### Issue description
`onIsOpenChange` in the combobox hooks invokes `onMenuClose` when the menu opens and `onMenuOpen` when it closes.

### Issue Context
This is inconsistent with the non-combobox dropdown hooks and will cause consumer callbacks (analytics, form wiring, etc.) to fire on the wrong transition.

### Fix Focus Areas
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[67-74]
- packages/core/src/components/Dropdown/hooks/useDropdownMultiCombobox.ts[80-84]

### Proposed change
- Replace `isOpen ? onMenuClose?.() : onMenuOpen?.();` with `isOpen ? onMenuOpen?.() : onMenuClose?.();` in both hooks (keep the filter-reset logic intact).

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

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

Copy link
Copy Markdown
Contributor

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

Open the overflow dialog reliably on Enter (drop "enter" as a hide trigger so
the keyup no longer toggles it shut), move focus to the first chip when it
opens, and return focus to the +N counter when it is dismissed with Esc.
Document the keyboard behavior alongside the InteractiveChipsOverflow story.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines 18 to 25
disabled?: boolean;
readOnly?: boolean;
minVisibleCount?: number;
/** Extra props (tabIndex, onKeyDown, etc.) to spread on each visible chip container. */
getChipContainerProps?: (item: Item, index: number) => Record<string, any>;
/** Ref forwarded to the +N overflow Chips element, for external keyboard focus management. */
badgeRef?: React.Ref<HTMLDivElement>;
};

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.

Remediation recommended

1. multiselectedvaluesprops defined inline 📘 Rule violation ⚙ Maintainability

MultiSelectedValues prop types were expanded inside the implementation file instead of being
defined in a dedicated *.types.ts file. This reduces consistency with the repo typing conventions
and makes the component API harder to discover and maintain.
Agent Prompt
## Issue description
`MultiSelectedValues` props are declared and updated inline in `MultiSelectedValues.tsx` rather than being defined in a dedicated `*.types.ts` file, which conflicts with the repository typing convention.

## Issue Context
The PR adds new props (`getChipContainerProps`, `badgeRef`) to `MultiSelectedValues`, so this is a good time to align the component typing structure.

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

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

Comment on lines +19 to +29
const DropdownInput = ({
inputSize,
fullWidth,
onKeyDown: externalKeyDown,
inputRef: externalInputRef
}: {
inputSize?: "small" | "medium" | "large";
fullWidth?: boolean;
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
inputRef?: RefObject<HTMLInputElement>;
}) => {

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.

Remediation recommended

2. dropdowninput props typed inline 📘 Rule violation ⚙ Maintainability

DropdownInput introduces/extends its props typing inline in the implementation file rather than
using a colocated *.types.ts file. This diverges from the repo’s standardized typing pattern and
complicates consistent reuse across the codebase.
Agent Prompt
## Issue description
`DropdownInput` defines its props type inline in `DropdownInput.tsx` (including newly added props like `onKeyDown` and `inputRef`) instead of using a dedicated `*.types.ts` file.

## Issue Context
This PR expands the component’s API surface (e.g., `onKeyDown`, `inputRef`) and updates accessibility behavior, making it an appropriate moment to standardize the typing location.

## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[19-29]

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

Comment on lines +36 to +64
const renderTriggerContent = () => {
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}
/>

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.

Remediation recommended

3. Disabled chips still focusable 🐞 Bug ≡ Correctness

interactiveChips is enabled even when disabled is true, so the input becomes non-focusable while
chip containers may still receive keyboard-focus/handlers via getSelectedItemProps. This creates
inconsistent disabled behavior and can leave focusable elements inside a disabled control.
Agent Prompt
### Issue description
When `interactiveChips` is enabled, the new interactive-chips branch activates for searchable multi-select even if the Dropdown is `disabled`. The input is disabled (not focusable), but chip containers may still become focusable/interactive via `getSelectedItemProps`, leading to inconsistent disabled semantics.

### Issue Context
- The interactive branch is gated by `interactiveChips && searchable && !readOnly`, but not `!disabled`.
- Chip containers spread `getSelectedItemProps` output, which is intended to add keyboard interaction props.

### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[36-67]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[108-139]
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[69-103]

### Suggested fix
- Add `!disabled` to the interactive branch condition, OR
- When `disabled`, pass `getChipContainerProps={undefined}` (or strip `tabIndex`/keydown handlers) so chips cannot receive focus while disabled.

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


addSelectedItem?: (item: Item) => void;
removeSelectedItem?: (item: Item) => void;
getSelectedItemProps?: (options: { selectedItem: any; index: number }) => Record<string, any>;

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.

Informational

4. Selected item typed as any 🐞 Bug ⚙ Maintainability

DropdownContextProps defines getSelectedItemProps with selectedItem: any, which removes the
generic Item type safety for the new interactive chips plumbing. This can mask type mismatches
when consumers pass custom item shapes.
Agent Prompt
### Issue description
`getSelectedItemProps` is added to the Dropdown context but its signature uses `selectedItem: any`, weakening type checking across the context API.

### Issue Context
The context is already generic (`DropdownContextProps<Item extends BaseItemData<...>>`), so `selectedItem` can and should be typed as `Item`.

### Fix Focus Areas
- packages/core/src/components/Dropdown/context/DropdownContext.types.ts[76-83]

### Suggested fix
Change:
```ts
getSelectedItemProps?: (options: { selectedItem: any; index: number }) => Record<string, any>;
```
To:
```ts
getSelectedItemProps?: (options: { selectedItem: Item; index: number }) => Record<string, any>;
```
(Optionally also tighten the return type away from `any` if feasible.)

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

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

Copy link
Copy Markdown
Contributor

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

Referencing the listbox or the combobox input never produced a usable name (a
textbox computes its name from its value, not its label). The shared chevron now
uses aria-labelledby to the visible label when present, and otherwise a real
aria-label string from aria-label / inputAriaLabel. Covers single and multi
searchable. Update tests and the single/multi a11y docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +755 to +756
expect(queryByTestId("dropdown-chip-opt1")).not.toBeInTheDocument();
expect(getByTestId("dropdown-chip-opt3")).toBeInTheDocument();

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.

Remediation recommended

1. dropdown-chip-* test ids hardcoded 📘 Rule violation ⚙ Maintainability

The updated Dropdown tests use hard-coded data-testid strings (e.g. dropdown-chip-opt1) instead
of the established test-id constants/patterns, reducing consistency and making tests more brittle to
refactors. This violates the requirement to follow repository test ID patterns from the centralized
constants.
Agent Prompt
## Issue description
Dropdown tests introduced/updated in this PR assert against hard-coded `data-testid` values (e.g. `dropdown-chip-opt1`). The compliance rule requires following established test-id patterns/constants to keep tests consistent and maintainable.

## Issue Context
The repo maintains centralized test-id constants in `packages/core/src/tests/constants.ts` (e.g. `ComponentDefaultTestId.DROPDOWN`, `ComponentDefaultTestId.CHIP`). Tests and/or component test-id generation should reuse these constants (or a derived, component-specific constant built from them) instead of embedding ad-hoc strings.

## Fix Focus Areas
- packages/core/src/components/Dropdown/__tests__/Dropdown.test.tsx[755-756]
- packages/core/src/tests/constants.ts[14-37]

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

Comment on lines +75 to +80
"aria-labelledby": label ? getLabelProps().id : undefined,
// When there is no visible label, the input must still have a name; fall back to the
// field's aria-label so the input — and the chevron that points at it — are named.
"aria-label": inputAriaLabel || (label ? undefined : ariaLabel),
"aria-describedby": describedBy,
// The menu is presented in a Dialog, so the combobox advertises a dialog popup.

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.

Remediation recommended

2. Inputarialabel ignored 🐞 Bug ≡ Correctness

DropdownInput always sets aria-labelledby when label is present, so the input’s accessible
name is forced to the label and inputAriaLabel cannot rename the inner combobox as documented.
This breaks the documented accessibility API contract for consumers needing a distinct input name.
Agent Prompt
### Issue description
`DropdownInput` sets both `aria-labelledby` (when `label` exists) and `aria-label` (when `inputAriaLabel` exists). Because `aria-labelledby` takes precedence for accessible-name computation, `inputAriaLabel` becomes ineffective whenever a visible label is present.

### Issue Context
The docs explicitly state `inputAriaLabel` is "useful when the input needs a name distinct from the field label".

### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[73-86]
- packages/docs/src/pages/components/Dropdown/DropdownSearchableSingleSelect.mdx[95-104]

### Proposed fix
Adjust name wiring so `inputAriaLabel` truly controls the input name:
- If `inputAriaLabel` is provided, do **not** set `aria-labelledby` on the input (or otherwise ensure the computed name uses `inputAriaLabel`).
- Otherwise, keep the current behavior: use `aria-labelledby` when `label` exists, and fall back to the field `aria-label` when it doesn’t.

Example:
```ts
const ariaLabelledBy = !inputAriaLabel && label ? getLabelProps().id : undefined;
const ariaLabel = inputAriaLabel ?? (label ? undefined : ariaLabelProp);
```

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

Comment on lines +42 to +53
<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.

Remediation recommended

3. Overflow arrowleft mismatch 🐞 Bug ≡ Correctness

With interactiveChips enabled and selections overflowing, ArrowLeft from the empty input moves
focus to the +N overflow badge rather than the last visible chip, contradicting the documented
keyboard behavior. This adds an extra navigation step and diverges from the component’s stated
contract.
Agent Prompt
### Issue description
The `interactiveChips` documentation says ArrowLeft from the input moves focus to the last chip. With overflow, the current implementation focuses the `+N` badge first.

### Issue Context
- `MultiSelectTrigger` focuses `overflowBadgeRef` on ArrowLeft from an empty input.
- `MultiSelectedValues` has the necessary `itemRefs`/`visibleCount` to focus the last visible chip, but it does not handle ArrowLeft on the input wrapper.

### Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/MultiSelectTrigger.tsx[42-65]
- packages/core/src/components/Dropdown/components/MultiSelectedValues/MultiSelectedValues.tsx[158-213]
- packages/core/src/components/Dropdown/Dropdown.types.ts[23-28]

### Proposed fix
Move/implement the ArrowLeft handling closer to where chip refs exist:
1. Add an `onKeyDown` handler on the `renderInput` wrapper inside `MultiSelectedValues` that:
   - checks for `e.key === 'ArrowLeft'` and that the actual input is empty,
   - `e.preventDefault()` and focuses `itemRefs[visibleCount - 1].current`.
2. Remove (or narrow) the `MultiSelectTrigger` ArrowLeft handler that focuses the overflow badge.

This makes the behavior match the documented contract while still allowing access to the overflow badge via normal navigation.

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

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

Copy link
Copy Markdown
Contributor

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

Focus never leaves the input, so Space would type a literal space and corrupt
the filter text. On Space, when an option is highlighted (aria-activedescendant
set — the user arrowed to it), prevent the default and select it via downshift's
selectItem; while typing (no highlight) Space types normally. Handled natively in
the getInputProps keydown for both single and multi. Tests + a11y docs added.

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

qodo-free-for-open-source-projects Bot commented Jun 30, 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

Merge the multiple "What changed — 2026-06-30" subsections on the single- and
multi-select accessibility pages into a single dated section with a list of all
fixes. No content changes beyond the regrouping; older dated sections untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +9 to +17
// Builds the screen-reader announcement of the current multi-select selection (the chip labels),
// surfaced to the combobox via aria-describedby + a visually hidden element. This is how the selected
// chips are made accessible without depending on the chip buttons themselves carrying the semantics.
function getSelectedValueText(selectedItems: BaseItemData[]): string {
return selectedItems
.map(item => item.label || item.value || "")
.filter(Boolean)
.join(", ");
}

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.

Remediation recommended

1. Selection text drops 0 🐞 Bug ≡ Correctness

DropdownInput.getSelectedValueText uses truthy fallbacks (item.label || item.value || "" +
filter(Boolean)), so a valid numeric value like 0 is treated as falsy and omitted from the
selection announcement. This can cause the multi-select selection to be announced incorrectly (or as
blank) for items with an empty/missing label and value 0.
Agent Prompt
## Issue description
`getSelectedValueText()` currently relies on truthiness (`||` + `filter(Boolean)`), which drops legitimate values like numeric `0` from the computed screen-reader announcement.

## Issue Context
- `BaseItemData.value` is `string | number`, so `0` is a valid value.
- The selection announcement is used as text content of a visually-hidden element referenced by `aria-describedby`.

## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[9-17]
- packages/core/src/components/BaseItem/BaseItem.types.ts[60-69]

## Suggested fix
- Prefer label when present, but use nullish coalescing instead of `||`, and always stringify numeric values:
  - e.g. `const text = item.label ?? String(item.value ?? "")`
  - then filter using `text.length > 0` (not `Boolean(text)`), or keep `0` explicitly.

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

Comment on lines +142 to +146
getInputProps: (options?: Parameters<typeof getInputProps>[0]) =>
getInputProps({
...options,
ref: inputRef,
onKeyDown: event => {

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.

Remediation recommended

2. Inputref prop ignored 🐞 Bug ≡ Correctness

DropdownInput accepts an inputRef prop and passes it to Downshift via getInputProps, but
useDropdownCombobox overwrites any provided ref with its internal inputRef. As a result,
consumer refs never receive the input element in searchable single-select, breaking
focus/measurement integrations.
Agent Prompt
## Issue description
`DropdownInput` now supports `inputRef`, but in the searchable single-select path the hook wrapper unconditionally sets `ref: inputRef`, overriding the ref provided by the caller. This makes `inputRef` ineffective.

## Issue Context
- `DropdownInput` passes a `ref` via `getInputProps({ ref: inputRef })`.
- `useDropdownCombobox` returns a wrapped `getInputProps` that always sets `ref: inputRef`.

## Fix Focus Areas
- packages/core/src/components/Dropdown/components/Trigger/DropdownInput.tsx[19-54]
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[24-26]
- packages/core/src/components/Dropdown/hooks/useDropdownCombobox.ts[142-159]

## Suggested fix
- Merge the internal and external refs instead of overwriting:
  - If `options?.ref` is a function, call it with the node.
  - If it is a ref object, assign to `.current`.
  - Always keep `inputRef.current` updated for internal focus management.
- Alternatively, change the hook API to accept an external ref and merge at the hook level.

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

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

Copy link
Copy Markdown
Contributor

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

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