diff --git a/packages/react/src/components/ComboBox/ComboBox-test.js b/packages/react/src/components/ComboBox/ComboBox-test.js
index c0de4bd07629..a1916be39a4c 100644
--- a/packages/react/src/components/ComboBox/ComboBox-test.js
+++ b/packages/react/src/components/ComboBox/ComboBox-test.js
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { render, screen, within, fireEvent, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
@@ -27,6 +27,32 @@ const openMenu = async () => {
const prefix = 'cds';
+const ControlledComboBox = ({ controlledItem }) => {
+ const items = generateItems(5, generateGenericItem);
+ const [value, setValue] = useState(controlledItem || items[0]);
+ const [onChangeCallCount, setOnChangeCallCount] = useState(0);
+ const controlledOnChange = ({ selectedItem }) => {
+ setValue(selectedItem);
+ setOnChangeCallCount(onChangeCallCount + 1);
+ };
+
+ return (
+
+
+
value: {value.label}
+
onChangeCallCount: {onChangeCallCount}
+
+
+ );
+};
+
describe('ComboBox', () => {
let mockProps;
window.HTMLElement.prototype.scrollIntoView = function () {};
@@ -309,7 +335,7 @@ describe('ComboBox', () => {
await waitForPosition();
expect(findInputNode()).toHaveDisplayValue(mockProps.items[1]);
});
- it('should update and call `onChange` when selection is updated from the combobox', async () => {
+ it('should update and call `onChange` once when selection is updated from the combobox', async () => {
render();
expect(mockProps.onChange).not.toHaveBeenCalled();
await openMenu();
@@ -319,7 +345,28 @@ describe('ComboBox', () => {
screen.getByRole('combobox', { value: 'Item 2' })
).toBeInTheDocument();
});
- it('should update and call `onChange` when selection is updated externally', async () => {
+ it('should not call `onChange` when current selection is selected again', async () => {
+ render();
+ expect(mockProps.onChange).not.toHaveBeenCalled();
+ await openMenu();
+ await userEvent.click(screen.getByRole('option', { name: 'Item 0' }));
+ expect(mockProps.onChange).toHaveBeenCalledTimes(0);
+ expect(
+ screen.getByRole('combobox', { value: 'Item 0' })
+ ).toBeInTheDocument();
+ });
+ it('should update and call `onChange` once when selection is updated from the combobox and the external state managing selectedItem is updated', async () => {
+ render();
+ expect(screen.getByText('onChangeCallCount: 0')).toBeInTheDocument();
+ await openMenu();
+ await userEvent.click(screen.getByRole('option', { name: 'Item 2' }));
+ expect(screen.getByText('onChangeCallCount: 1')).toBeInTheDocument();
+ expect(screen.getByText('value: Item 2')).toBeInTheDocument();
+ expect(
+ screen.getByRole('combobox', { value: 'Item 2' })
+ ).toBeInTheDocument();
+ });
+ it('should update and call `onChange` once when selection is updated externally', async () => {
const { rerender } = render(
);
@@ -334,7 +381,7 @@ describe('ComboBox', () => {
await userEvent.click(
screen.getByRole('button', { name: 'Clear selected item' })
);
- expect(mockProps.onChange).toHaveBeenCalled();
+ expect(mockProps.onChange).toHaveBeenCalledTimes(1);
expect(findInputNode()).toHaveDisplayValue('');
});
it('should clear selected item when `selectedItem` is updated to `null` externally', async () => {
diff --git a/packages/react/src/components/ComboBox/ComboBox.tsx b/packages/react/src/components/ComboBox/ComboBox.tsx
index d56c4b7b0c04..75f34be51f02 100644
--- a/packages/react/src/components/ComboBox/ComboBox.tsx
+++ b/packages/react/src/components/ComboBox/ComboBox.tsx
@@ -34,6 +34,7 @@ import {
WarningAltFilled,
WarningFilled,
} from '@carbon/icons-react';
+import isEqual from 'react-fast-compare';
import ListBox, {
PropTypes as ListBoxPropTypes,
ListBoxSize,
@@ -509,11 +510,15 @@ const ComboBox = forwardRef(
selectedItem: selectedItemProp,
prevSelectedItem: prevSelectedItemProp.current,
});
- setInputValue(currentInputValue);
- onChange({
- selectedItem: selectedItemProp,
- inputValue: currentInputValue,
- });
+
+ // selectedItem has been updated externally, need to update state and call onChange
+ if (inputValue !== currentInputValue) {
+ setInputValue(currentInputValue);
+ onChange({
+ selectedItem: selectedItemProp,
+ inputValue: currentInputValue,
+ });
+ }
prevSelectedItemProp.current = selectedItemProp;
}
}, [selectedItemProp]);
@@ -746,7 +751,10 @@ const ComboBox = forwardRef(
setHighlightedIndex(indexToHighlight(normalizedInput));
},
onSelectedItemChange({ selectedItem }) {
- onChange({ selectedItem });
+ // only call onChange if new selection is updated from previous
+ if (!isEqual(selectedItem, selectedItemProp)) {
+ onChange({ selectedItem });
+ }
},
onHighlightedIndexChange: ({ highlightedIndex }) => {
if (highlightedIndex! > -1 && typeof window !== undefined) {