Skip to content

useComboBoxState: commitCustomValue() unconditionally clears all selections when selectionMode="multiple" and allowsCustomValue={true} #10071

@krishnamacharishrikanth282

Description

Provide a general summary of the issue here

Package

react-aria-components / @react-stately/combobox

Summary

When selectionMode="multiple" and allowsCustomValue={true} are used together on a ComboBox, three keyboard interactions — Escape, Tab, and blur with non-empty input — silently wipe the entire current selection. This makes the combination effectively unusable for multi-select comboboxes that allow custom values.

Root Cause (located in source)

In useComboBoxState, the selectedKey variable is hardcoded to null for multiple selection mode:

// useComboBoxState.js
let selectedKey = selectionMode === 'single' ? selectionManager.firstSelectedKey : null;

This means itemText is always "" in multiple mode:

const itemText = selectedKey != null ? collection.getItem(selectedKey)?.textValue ?? '' : '';

So commitValue() always routes to commitCustomValue() whenever inputValue is non-empty:

const commitValue = () => {
  if (allowsCustomValue) {
    inputValue === itemText ? commitSelection() : commitCustomValue(); // itemText is always "" in multiple
  }
};

And commitCustomValue() unconditionally clears all selections in multiple mode:

let commitCustomValue = () => {
  let value = selectionMode === 'multiple' ? EMPTY_VALUE : null; // EMPTY_VALUE = []
  setValue(value);
  closeMenu();
};

Additionally, revert() — called directly on Escape — also routes to commitCustomValue() whenever allowsCustomValue && selectedKey == null, which is always true in multiple mode:

let revert = () => {
  if (allowsCustomValue && selectedKey == null) commitCustomValue(); // always true for multiple
  else commitSelection();
};

🤔 Expected Behavior?

Scenarios and Expected vs Actual Behavior

Bug 1 — Tab out clears entire selection (most critical)
Steps:

Select "India" and "Japan" from the list
Click the input, type any text (e.g. "Ca")
Press Tab to move focus to the next element
Expected: Tab commits or discards the typed text; previously selected items ("India", "Japan") remain selected.

Bug 2 — Escape clears entire selection
Steps:

Use keyboard (ArrowDown + Enter) to select "India" from the list
The popover remains open (expected in multiple mode)
Press Escape to dismiss the popover
Expected: Popover closes, "India" remains selected.

Bug 3 — Blur with non-empty input clears entire selection
Steps:

Select "India" from the list
Type "Ca" in the input (do not select anything)
Click outside the ComboBox
Expected: "Ca" is discarded (not a known item), "India" remains selected.

Bug 4 — Enter on a list item (without keyboard navigation) does not select it
Steps:

Type "India" (matching a list item); popover opens
Do not press ArrowDown; press Enter directly
Expected: "India" is selected (the text unambiguously matches one item).

😯 Current Behavior

Scenarios and Expected vs Actual Behavior

Bug 1 — Tab out clears entire selection (most critical)
Steps:

Select "India" and "Japan" from the list
Click the input, type any text (e.g. "Ca")
Press Tab to move focus to the next element
Actual: commit() → commitValue() → commitCustomValue() → setValue([]). Entire selection is wiped.

Bug 2 — Escape clears entire selection
Steps:

Use keyboard (ArrowDown + Enter) to select "India" from the list
The popover remains open (expected in multiple mode)
Press Escape to dismiss the popover
Actual: revert() → commitCustomValue() → setValue([]). "India" is removed.

Bug 3 — Blur with non-empty input clears entire selection
Steps:

Select "India" from the list
Type "Ca" in the input (do not select anything)
Click outside the ComboBox
Actual: blur → commitValue() → commitCustomValue() → setValue([]). "India" is removed.

Bug 4 — Enter on a list item (without keyboard navigation) does not select it
Steps:

Type "India" (matching a list item); popover opens
Do not press ArrowDown; press Enter directly
Actual: commit() → focusedKey == null → commitValue() → commitCustomValue() → setValue([]). Nothing is selected.

💁 Possible Solution

### Expected Fix
commitCustomValue() and revert() should not clear selectedKeys when selectionMode="multiple". The intent of commitCustomValue for multiple selection should be:

  • Clear inputValue and close the menu
  • Preserve the existing selectedKeys

For commitValue(), when selectionMode="multiple", itemText should not be derived from a single selectedKey. Either compare against an empty string only when inputValue is truly empty, or treat any typed text as a potential custom entry to add (not a reason to clear).

🔦 Context

No response

🖥️ Steps to Reproduce

Reproduction Steps

function Demo() {
  const [selectedKeys, setSelectedKeys] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const items = [
    { id: 1, name: 'India' },
    { id: 2, name: 'Japan' },
    { id: 3, name: 'Canada' },
  ];

  return (
    <ComboBox
      selectionMode="multiple"
      allowsCustomValue
      defaultItems={items}
      selectedKey={selectedKeys}
      onChange={setSelectedKeys}
      inputValue={inputValue}
      onInputChange={setInputValue}
    >
      <Label>Countries</Label>
      <Input />
      <Button />
      <Popover>
        <ListBox>
          {(item) => <ListBoxItem id={item.id}>{item.name}</ListBoxItem>}
        </ListBox>
      </Popover>
    </ComboBox>
  );
}

Version

react-aria-components@1.17.0

What browsers are you seeing the problem on?

Chrome

If other, please specify.

react-aria-components@1.17.0

What operating system are you using?

Mac OS

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions