useComboBoxState: commitCustomValue() unconditionally clears all selections when selectionMode="multiple" and allowsCustomValue={true}
- Dominant language
- TypeScript
- Stars
- 15.9k
- Forks
- 1.6k
- Avg merge
- 3d 5h
- Merged PRs (30d)
- 58
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 (
Countries
{(item) => {item.name}}
);
}
```
### 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_
Contributor guide
Research direction
Start in useComboBoxState.js and trace commitValue(), commitCustomValue(), and revert() for multiple selection with custom values. Reproduce the four listed keyboard and blur scenarios, then verify that typed input is cleared or committed without removing existing selected keys and that a matching item can be selected with Enter.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, react
- Domain
- accessibility, frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100