Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions packages/react/src/autocomplete/value/AutocompleteValue.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use client';
import * as React from 'react';
import { useStore } from '@base-ui-components/utils/store';
import { useComboboxRootContext } from '../../combobox/root/ComboboxRootContext';
import { selectors } from '../../combobox/store';
import { useComboboxInputValueContext } from '../../combobox/root/ComboboxRootContext';

/**
* The current value of the autocomplete.
Expand All @@ -13,9 +11,7 @@ import { selectors } from '../../combobox/store';
export function AutocompleteValue(props: AutocompleteValue.Props) {
const { children } = props;

const store = useComboboxRootContext();

const inputValue = useStore(store, selectors.inputValue);
const inputValue = useComboboxInputValueContext();

if (typeof children === 'function') {
return children(String(inputValue));
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/combobox/clear/ComboboxClear.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { useStore } from '@base-ui-components/utils/store';
import { useComboboxRootContext } from '../root/ComboboxRootContext';
import { useComboboxInputValueContext, useComboboxRootContext } from '../root/ComboboxRootContext';
import type { BaseUIComponentProps, NativeButtonProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
import { selectors } from '../store';
Expand Down Expand Up @@ -45,7 +45,8 @@ export const ComboboxClear = React.forwardRef(function ComboboxClear(
const clearRef = useStore(store, selectors.clearRef);
const open = useStore(store, selectors.open);
const selectedValue = useStore(store, selectors.selectedValue);
const inputValue = useStore(store, selectors.inputValue);

const inputValue = useComboboxInputValueContext();

let visible = false;
if (selectionMode === 'none') {
Expand Down
65 changes: 5 additions & 60 deletions packages/react/src/combobox/input/ComboboxInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useStore } from '@base-ui-components/utils/store';
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
import { BaseUIComponentProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
import { useComboboxRootContext } from '../root/ComboboxRootContext';
import { useComboboxInputValueContext, useComboboxRootContext } from '../root/ComboboxRootContext';
import { selectors } from '../store';
import { pressableTriggerOpenStateMapping } from '../../utils/popupStateMapping';
import { useFieldRootContext } from '../../field/root/FieldRootContext';
Expand Down Expand Up @@ -54,10 +54,10 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
const triggerProps = useStore(store, selectors.triggerProps);
const open = useStore(store, selectors.open);
const selectedValue = useStore(store, selectors.selectedValue);
const inputValue = useStore(store, selectors.inputValue);

const [composingValue, setComposingValue] = React.useState<string | null>(null);
const isComposingRef = React.useRef(false);
// `inputValue` can't be placed in the store.
// https://github.com/mui/base-ui/issues/2703
const inputValue = useComboboxInputValueContext();

const disabled = fieldDisabled || comboboxDisabled || disabledProp;

Expand Down Expand Up @@ -145,7 +145,7 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
triggerProps,
{
type: 'text',
value: componentProps.value ?? composingValue ?? inputValue,
value: inputValue,
'aria-readonly': readOnly || undefined,
'aria-labelledby': labelId,
disabled,
Expand All @@ -163,62 +163,7 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
fieldControlValidation?.commitValidation(valueToValidate);
}
},
onCompositionStart(event) {
isComposingRef.current = true;
setComposingValue(event.currentTarget.value);
},
onCompositionEnd(event) {
isComposingRef.current = false;
const next = event.currentTarget.value;
setComposingValue(null);
store.state.setInputValue(
next,
createBaseUIEventDetails('input-change', event.nativeEvent),
);
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @mj12albert to test with composed characters, none of this should be necessary now

Copy link
Member

@mj12albert mj12albert Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still works correctly~

Also noticed in the async search autocomplete demo undo/redo using the keyboard (after typing some stuff) is messed up but works correctly in this PR now

onChange(event: React.ChangeEvent<HTMLInputElement>) {
// During IME composition, avoid propagating controlled updates to preserve
// its state.
if (isComposingRef.current) {
const nextVal = event.currentTarget.value;
setComposingValue(nextVal);

if (nextVal === '' && !openOnInputClick && !hasPositionerParent) {
store.state.setOpen(
false,
createBaseUIEventDetails('input-clear', event.nativeEvent),
);
}

if (!readOnly && !disabled) {
const trimmed = nextVal.trim();
if (trimmed !== '') {
store.state.setOpen(true, createBaseUIEventDetails('none', event.nativeEvent));
if (!(selectionMode === 'none' && autoHighlight)) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer',
});
}
}
}

if (
open &&
store.state.activeIndex !== null &&
!(selectionMode === 'none' && autoHighlight && nextVal.trim() !== '')
) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer',
});
}

return;
}

store.state.setInputValue(
event.currentTarget.value,
createBaseUIEventDetails('input-change', event.nativeEvent),
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/combobox/root/ComboboxRootContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const ComboboxFloatingContext = React.createContext<FloatingRootContext |
export const ComboboxDerivedItemsContext = React.createContext<
ComboboxDerivedItemsContext | undefined
>(undefined);
// `inputValue` can't be placed in the store.
// https://github.com/mui/base-ui/issues/2703
export const ComboboxInputValueContext =
React.createContext<React.ComponentProps<'input'>['value']>('');

export function useComboboxRootContext() {
const context = React.useContext(ComboboxRootContext) as ComboboxStore | undefined;
Expand Down Expand Up @@ -45,3 +49,7 @@ export function useComboboxDerivedItemsContext() {
}
return context;
}

export function useComboboxInputValueContext() {
return React.useContext(ComboboxInputValueContext);
}
17 changes: 10 additions & 7 deletions packages/react/src/combobox/root/ComboboxRootInternal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ComboboxFloatingContext,
ComboboxDerivedItemsContext,
ComboboxRootContext,
ComboboxInputValueContext,
} from './ComboboxRootContext';
import { selectors, type State as StoreState } from '../store';
import { useOpenChangeComplete } from '../../utils/useOpenChangeComplete';
Expand Down Expand Up @@ -1027,13 +1028,15 @@ export function ComboboxRootInternal<Value = any, Mode extends SelectionMode = '
<ComboboxRootContext.Provider value={store}>
<ComboboxFloatingContext.Provider value={floatingRootContext}>
<ComboboxDerivedItemsContext.Provider value={itemsContextValue}>
{virtualized ? (
children
) : (
<CompositeList elementsRef={listRef} labelsRef={items ? undefined : labelsRef}>
{children}
</CompositeList>
)}
<ComboboxInputValueContext.Provider value={inputValue}>
{virtualized ? (
children
) : (
<CompositeList elementsRef={listRef} labelsRef={items ? undefined : labelsRef}>
{children}
</CompositeList>
)}
</ComboboxInputValueContext.Provider>
</ComboboxDerivedItemsContext.Provider>
</ComboboxFloatingContext.Provider>
</ComboboxRootContext.Provider>
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/combobox/trigger/ComboboxTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTimeout } from '@base-ui-components/utils/useTimeout';
import { BaseUIComponentProps, NativeButtonProps } from '../../utils/types';
import { useRenderElement } from '../../utils/useRenderElement';
import { useButton } from '../../use-button';
import { useComboboxRootContext } from '../root/ComboboxRootContext';
import { useComboboxInputValueContext, useComboboxRootContext } from '../root/ComboboxRootContext';
import { selectors } from '../store';
import { useFieldRootContext } from '../../field/root/FieldRootContext';
import { pressableTriggerOpenStateMapping } from '../../utils/popupStateMapping';
Expand Down Expand Up @@ -57,7 +57,8 @@ export const ComboboxTrigger = React.forwardRef(function ComboboxTrigger(
const inputInsidePopup = useStore(store, selectors.inputInsidePopup);
const open = useStore(store, selectors.open);
const selectedValue = useStore(store, selectors.selectedValue);
const inputValue = useStore(store, selectors.inputValue);

const inputValue = useComboboxInputValueContext();

const disabled = fieldDisabled || comboboxDisabled || disabledProp;

Expand Down
Loading