mui / mui/base-ui

[combobox] Item selected with the mouse during IME composition leaves stale text in the input

Open
#5,574 0 comments 0 reactions 0 assignees View on GitHub
component: combobox type: bug
Dominant language
TypeScript
Stars
10.9k
Forks
543
Avg merge
1d 20h
Merged PRs (30d)
101

Description

# Bug report

## Current behavior

When an IME composition is active in `Combobox.Input` (typing Korean/Japanese/Chinese), selecting an item **with the mouse** commits the value but leaves the input showing the in-progress composition text.

```
type "창" with a Korean IME (composition still active, not yet committed)
click the "창세기" item
→ onValueChange fires, selected value === "창세기" ✅
→ input still displays "창" ❌
```

To the user this reads as "the click didn't register". The natural next action is to delete the leftover text and try again — and because emptying the input clears the selection (`combobox/input/ComboboxInput.js:272`, `setSelectedValue(null)`), a required field in a form then reports *no selection at all*. So a display-only glitch turns into real data loss.

Keyboard selection is unaffected: Enter commits the composition first, so there is no composition left to win over the filled-in label.

This only reproduces through an actual IME composition. Typing the same characters programmatically (or with a non-IME keyboard) does not reproduce it.

## Expected behavior

After an item is pressed, the input shows the selected item's label — the same as when no composition is active.

## Reproducible example

I could not exercise an IME inside CodeSandbox, so here is a standalone minimal app plus a scripted reproduction that drives a real composition through CDP.

`main.jsx` — stock components, no wrappers:

```jsx
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
import { Combobox } from '@base-ui/react';

const BOOKS = ['창세기', '출애굽기', '레위기', '민수기', '신명기'];

function App() {
const [value, setValue] = useState(null);
return (








{(item) => (

{item}

)}





selected: {String(value)}



);
}
createRoot(document.getElementById('root')).render();
```

**Manual steps** (macOS, Korean 2-Set input): focus the input, type `ㅊ` `ㅏ` `ㅇ` so that `창` is still underlined/composing, then click `창세기` in the popup. The paragraph shows `selected: 창세기` while the input still shows `창`.

**Scripted** (Playwright + CDP `Input.imeSetComposition`, which produces a genuine composition):

```js
const cdp = await context.newCDPSession(page);
await page.locator('#book').click();
for (const s of ['ㅊ', '차', '창']) {
await cdp.send('Input.imeSetComposition', { text: s, selectionStart: s.length, selectionEnd: s.length });
}
await page.locator('[role="option"]', { hasText: '창세기' }).first().click();

console.log(await page.locator('#book').inputValue()); // "창" ← expected "창세기"
console.log(await page.locator('#state').innerText()); // "selected: 창세기"
```

Output I get, with the two non-composing paths for contrast:

```
composition active, mouse click input="창" selected: 창세기 ← mismatch
composition committed, click input="창세기" selected: 창세기
no typing, click input="창세기" selected: 창세기
```

## Base UI version

v1.7.0. Also reproduces on v1.3.0 (where I first hit it).

## Which browser are you using?

Chrome 152.0.7977.65

## Which OS are you using?

macOS 26.6.2

## Which assistive tech are you using (if applicable)?

None.

## Additional context

What I found while tracing it, in case it's useful — line numbers are from v1.7.0.

1. `ComboboxInput` keeps the composing string in local state and renders it in preference to the store value:

```js
// combobox/input/ComboboxInput.js:167
value: composingValue ?? inputValue,
```

`composingValue` is only cleared in `onCompositionEnd` (`:219`).

2. Pressing an item fills only the **store's** input value:

```js
// combobox/root/AriaCombobox.js:562-565
const shouldFillInput = ... || single && !store.state.inputInsidePopup;
if (shouldFillInput) setInputValue(stringifyAsLabel(nextValue, itemToStringLabel), ...);
```

3. `ComboboxItem`'s `onPointerDownCapture` calls `preventDefault()` (`:131`), which is what keeps focus in the input — so the click never ends the composition. `composingValue` is still set when the label lands in the store, and step 1 keeps rendering the stale composition.

So the two mechanisms are individually reasonable; they just collide on pointer selection. The composition guard added in #2942 is clearly deliberate (avoid filtering on partial jamo) and I don't think it's the thing to change.

If it helps, the shape of a fix might be either letting an item press clear `composingValue` (the user has chosen an item, so the in-flight query is moot), or having the fill-on-press path win over `composingValue` specifically for `REASONS.itemPress`. I'm happy to open a PR if you can confirm which direction you'd prefer.

Two smaller notes from the same trace:

- The follow-on severity comes from `:272` — emptying the input clears the selection in single mode. That's sensible on its own, but combined with this bug the user's "let me clear it and retry" reflex silently drops an already-valid selection.
- As a userland workaround, passing `value` to `Combobox.Input` still wins, because element props are merged last (`props: [inputProps, triggerProps, { ... }, validationProps]`). Worth noting that v1.3.0 spelled this out explicitly as `componentProps.value ?? composingValue ?? inputValue` and v1.7.0 no longer does — so the workaround now depends on merge order rather than on anything intentional.

Contributor guide

Open the contributing guide

Research direction

Start in combobox/input/ComboboxInput.js around the composingValue rendering and onCompositionEnd logic, then trace combobox/root/AriaCombobox.js and ComboboxItem's pointer-press handling. Reproduce the issue with the supplied Playwright CDP composition sequence. Done means a mouse selection during active IME composition displays the selected item's label while keyboard selection and non-composing paths remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.