Skip to content

Commit 1e7473e

Browse files
authored
[combobox] Fix popup input form submit (#4687)
1 parent 7019fdb commit 1e7473e

3 files changed

Lines changed: 104 additions & 5 deletions

File tree

packages/react/src/combobox/input/ComboboxInput.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
import { triggerStateAttributesMapping } from '../utils/stateAttributesMapping';
1515
import { selectors } from '../store';
1616
import type { FieldRootState } from '../../field/root/FieldRoot';
17-
import { useFieldRootContext } from '../../internals/field-root-context/FieldRootContext';
17+
import {
18+
DEFAULT_FIELD_ROOT_CONTEXT,
19+
FieldRootContext,
20+
useFieldRootContext,
21+
} from '../../internals/field-root-context/FieldRootContext';
1822
import { DEFAULT_FIELD_STATE_ATTRIBUTES } from '../../internals/field-constants/constants';
1923
import { useLabelableContext } from '../../internals/labelable-provider/LabelableContext';
2024
import { useComboboxChipsContext } from '../chips/ComboboxChipsContext';
@@ -484,12 +488,20 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
484488
stateAttributesMapping: triggerStateAttributesMapping,
485489
});
486490

491+
const renderedInput = hasPositionerParent ? (
492+
<FieldRootContext.Provider value={DEFAULT_FIELD_ROOT_CONTEXT}>
493+
{element}
494+
</FieldRootContext.Provider>
495+
) : (
496+
element
497+
);
498+
487499
return (
488500
<React.Fragment>
489501
{open && focusManagerModal && (
490502
<ComboboxInternalDismissButton ref={store.state.startDismissRef} />
491503
)}
492-
{element}
504+
{renderedInput}
493505
</React.Fragment>
494506
);
495507
});

packages/react/src/combobox/root/ComboboxRoot.test.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Combobox } from '@base-ui/react/combobox';
1414
import { Dialog } from '@base-ui/react/dialog';
1515
import { Field } from '@base-ui/react/field';
1616
import { Form } from '@base-ui/react/form';
17+
import { Input } from '@base-ui/react/input';
1718
import { useStore } from '@base-ui/utils/store';
1819
import { CompositeRoot } from '../../internals/composite/root/CompositeRoot';
1920
import { CompositeItem } from '../../internals/composite/item/CompositeItem';
@@ -4971,7 +4972,7 @@ describe('<Combobox.Root />', () => {
49714972
<Combobox.Portal>
49724973
<Combobox.Positioner>
49734974
<Combobox.Popup>
4974-
<Combobox.Input data-testid="input" />
4975+
<Combobox.Input render={<Input data-testid="input" />} />
49754976
<Combobox.List>
49764977
<Combobox.Item value="a">a</Combobox.Item>
49774978
<Combobox.Item value="b">b</Combobox.Item>
@@ -5008,6 +5009,90 @@ describe('<Combobox.Root />', () => {
50085009
expect(input).not.toHaveAttribute('data-invalid');
50095010
});
50105011

5012+
it('submits when input renders a field-aware input', async () => {
5013+
const handleFormSubmit = vi.fn();
5014+
5015+
const { user } = await render(
5016+
<Form onFormSubmit={handleFormSubmit}>
5017+
<Field.Root name="country">
5018+
<Combobox.Root items={['France', 'Germany']} required>
5019+
<Combobox.Input render={<Input data-testid="input" />} />
5020+
<Combobox.Portal>
5021+
<Combobox.Positioner>
5022+
<Combobox.Popup>
5023+
<Combobox.List>
5024+
{(item: string) => (
5025+
<Combobox.Item key={item} value={item}>
5026+
{item}
5027+
</Combobox.Item>
5028+
)}
5029+
</Combobox.List>
5030+
</Combobox.Popup>
5031+
</Combobox.Positioner>
5032+
</Combobox.Portal>
5033+
</Combobox.Root>
5034+
</Field.Root>
5035+
<button type="submit">Submit</button>
5036+
</Form>,
5037+
);
5038+
5039+
const input = screen.getByTestId('input');
5040+
expect(input).toHaveAttribute('name', 'country');
5041+
5042+
await user.click(input);
5043+
await user.click(screen.getByRole('option', { name: 'France' }));
5044+
await user.click(screen.getByText('Submit'));
5045+
5046+
expect(handleFormSubmit.mock.calls.length).toBe(1);
5047+
expect(handleFormSubmit.mock.calls[0][0]).toEqual({ country: 'France' });
5048+
});
5049+
5050+
it('submits when input inside popup renders a field-aware input', async () => {
5051+
const handleFormSubmit = vi.fn();
5052+
5053+
const { user } = await render(
5054+
<Form onFormSubmit={handleFormSubmit}>
5055+
<Field.Root name="country">
5056+
<Combobox.Root items={['France', 'Germany']} required>
5057+
<Combobox.Trigger>
5058+
<Combobox.Value />
5059+
</Combobox.Trigger>
5060+
<Combobox.Portal>
5061+
<Combobox.Positioner>
5062+
<Combobox.Popup>
5063+
<Combobox.Input render={<Input data-testid="input" />} />
5064+
<Combobox.List>
5065+
{(item: string) => (
5066+
<Combobox.Item key={item} value={item}>
5067+
{item}
5068+
</Combobox.Item>
5069+
)}
5070+
</Combobox.List>
5071+
</Combobox.Popup>
5072+
</Combobox.Positioner>
5073+
</Combobox.Portal>
5074+
</Combobox.Root>
5075+
</Field.Root>
5076+
<button type="submit">Submit</button>
5077+
</Form>,
5078+
);
5079+
5080+
await user.click(screen.getByRole('combobox'));
5081+
const input = await screen.findByTestId('input');
5082+
expect(input).not.toHaveAttribute('name');
5083+
5084+
await user.click(screen.getByRole('option', { name: 'France' }));
5085+
5086+
const hiddenInput = screen.getByRole('textbox', { hidden: true });
5087+
expect(hiddenInput).toHaveAttribute('name', 'country');
5088+
expect(hiddenInput).toHaveValue('France');
5089+
5090+
await user.click(screen.getByText('Submit'));
5091+
5092+
expect(handleFormSubmit.mock.calls.length).toBe(1);
5093+
expect(handleFormSubmit.mock.calls[0][0]).toEqual({ country: 'France' });
5094+
});
5095+
50115096
it('clears external errors on change', async () => {
50125097
const { user } = await renderFakeTimers(
50135098
<Form

packages/react/src/internals/field-root-context/FieldRootContext.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export interface FieldRootContext {
4343
validation: UseFieldValidationReturnValue;
4444
}
4545

46-
export const FieldRootContext = React.createContext<FieldRootContext>({
46+
export const DEFAULT_FIELD_ROOT_CONTEXT: FieldRootContext = {
4747
invalid: undefined,
4848
name: undefined,
4949
validityData: {
@@ -76,7 +76,9 @@ export const FieldRootContext = React.createContext<FieldRootContext>({
7676
inputRef: { current: null },
7777
commit: async () => {},
7878
},
79-
});
79+
};
80+
81+
export const FieldRootContext = React.createContext<FieldRootContext>(DEFAULT_FIELD_ROOT_CONTEXT);
8082

8183
export function useFieldRootContext(optional = true) {
8284
const context = React.useContext(FieldRootContext);

0 commit comments

Comments
 (0)