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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions