Skip to content

Commit 84fd364

Browse files
authored
fix: use initialSelectedItem only on initial render in ComboBox (#18602)
* fix: use initialSelectedItem only on initial render * feat: add test story
1 parent b67877b commit 84fd364

File tree

3 files changed

+74
-13
lines changed

3 files changed

+74
-13
lines changed

packages/react/src/components/ComboBox/ComboBox-test.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -329,6 +329,40 @@ describe('ComboBox', () => {
329329
await waitForPosition();
330330
expect(findInputNode()).toHaveDisplayValue(mockProps.items[1]);
331331
});
332+
333+
it('should not revert to initialSelectedItem after clearing selection in uncontrolled mode', async () => {
334+
// Render a non-fully controlled `ComboBox` using `initialSelectedItem`.
335+
render(
336+
<ComboBox {...mockProps} initialSelectedItem={mockProps.items[0]} />
337+
);
338+
await waitForPosition();
339+
// Verify that the input initially displays `initialSelectedItem`.
340+
expect(findInputNode()).toHaveDisplayValue(mockProps.items[0].label);
341+
342+
// Simulate clearing the selection by clicking the clear button.
343+
await userEvent.click(
344+
screen.getByRole('button', { name: 'Clear selected item' })
345+
);
346+
// After clearing, the input should be empty rather than reverting to
347+
// `initialSelectedItem`.
348+
expect(findInputNode()).toHaveDisplayValue('');
349+
});
350+
351+
it('should ignore updates to initialSelectedItem after initial render in uncontrolled mode', async () => {
352+
// Render a non-fully controlled `ComboBox` using `initialSelectedItem`.
353+
const { rerender } = render(
354+
<ComboBox {...mockProps} initialSelectedItem={mockProps.items[0]} />
355+
);
356+
await waitForPosition();
357+
expect(findInputNode()).toHaveDisplayValue(mockProps.items[0].label);
358+
359+
// Rerender the component with a different `initialSelectedItem`.
360+
rerender(
361+
<ComboBox {...mockProps} initialSelectedItem={mockProps.items[2]} />
362+
);
363+
// The displayed value should still be the one from the first render.
364+
expect(findInputNode()).toHaveDisplayValue(mockProps.items[0].label);
365+
});
332366
});
333367

334368
describe('provided `selectedItem`', () => {

packages/react/src/components/ComboBox/ComboBox.stories.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,32 @@ export const _fullyControlled = (args) => {
410410

411411
_fullyControlled.argTypes = { ...sharedArgTypes };
412412

413+
export const _fullyControlled2 = () => {
414+
const [selectedItem, setSelectedItem] = useState(null);
415+
416+
return (
417+
<div
418+
style={{
419+
display: 'flex',
420+
flexDirection: 'column',
421+
gap: '1rem',
422+
width: '256px',
423+
}}>
424+
<ComboBox
425+
id="carbon-combobox"
426+
items={['1', '2', '3']}
427+
onChange={({ selectedItem }) => setSelectedItem(selectedItem)}
428+
selectedItem={selectedItem}
429+
titleText="Fully Controlled ComboBox title"
430+
/>
431+
<Button kind="danger" onClick={() => setSelectedItem(null)} size="md">
432+
Reset
433+
</Button>
434+
<p>Selected value: {`${selectedItem}`}</p>
435+
</div>
436+
);
437+
};
438+
413439
AutocompleteWithTypeahead.argTypes = {
414440
onChange: { action: 'onChange' },
415441
};

packages/react/src/components/ComboBox/ComboBox.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -100,30 +100,33 @@ const autocompleteCustomFilter = ({
100100

101101
const getInputValue = <ItemType,>({
102102
initialSelectedItem,
103-
inputValue,
104103
itemToString,
105104
selectedItem,
106105
prevSelectedItem,
107106
}: {
108107
initialSelectedItem?: ItemType | null;
109-
inputValue: string;
110108
itemToString: ItemToStringHandler<ItemType>;
111109
selectedItem?: ItemType | null;
112110
prevSelectedItem?: ItemType | null;
113111
}) => {
114-
if (selectedItem) {
112+
// If there's a current selection (even if it's an object or string), use it.
113+
if (selectedItem !== null && typeof selectedItem !== 'undefined') {
115114
return itemToString(selectedItem);
116115
}
117116

118-
if (initialSelectedItem) {
117+
// On the very first render (when no previous value exists), use
118+
// `initialSelectedItem`.
119+
if (
120+
typeof prevSelectedItem === 'undefined' &&
121+
initialSelectedItem !== null &&
122+
typeof initialSelectedItem !== 'undefined'
123+
) {
119124
return itemToString(initialSelectedItem);
120125
}
121126

122-
if (!selectedItem && prevSelectedItem) {
123-
return '';
124-
}
125-
126-
return inputValue || '';
127+
// Otherwise (i.e., after the user has cleared the selection), return an empty
128+
// string.
129+
return '';
127130
};
128131

129132
const findHighlightedIndex = <ItemType,>(
@@ -463,7 +466,6 @@ const ComboBox = forwardRef(
463466
const [inputValue, setInputValue] = useState(
464467
getInputValue({
465468
initialSelectedItem,
466-
inputValue: '',
467469
itemToString,
468470
selectedItem: selectedItemProp,
469471
})
@@ -512,7 +514,6 @@ const ComboBox = forwardRef(
512514
if (prevSelectedItemProp.current !== selectedItemProp) {
513515
const currentInputValue = getInputValue({
514516
initialSelectedItem,
515-
inputValue,
516517
itemToString,
517518
selectedItem: selectedItemProp,
518519
prevSelectedItem: prevSelectedItemProp.current,

0 commit comments

Comments
 (0)