diff --git a/packages/react/src/components/ComboBox/ComboBox-test.js b/packages/react/src/components/ComboBox/ComboBox-test.js
index c2d07c34703e..1fb29ed4eead 100644
--- a/packages/react/src/components/ComboBox/ComboBox-test.js
+++ b/packages/react/src/components/ComboBox/ComboBox-test.js
@@ -423,4 +423,69 @@ describe('ComboBox', () => {
);
});
});
+ describe('ComboBox autocomplete', () => {
+ const items = [
+ { id: 'option-1', text: 'Option 1' },
+ { id: 'option-2', text: 'Option 2' },
+ { id: 'option-3', text: 'Option 3' },
+ ];
+
+ const mockProps = {
+ id: 'test-combobox',
+ items,
+ itemToString: (item) => (item ? item.text : ''),
+ onChange: jest.fn(),
+ };
+
+ it('should respect autocomplete prop', async () => {
+ render();
+ await waitForPosition();
+ const inputNode = findInputNode();
+ expect(inputNode).toHaveAttribute('autocomplete');
+ });
+ it('should use autocompleteCustomFilter when autocomplete prop is true', async () => {
+ render();
+
+ // Open the dropdown
+ const input = screen.getByRole('combobox');
+ fireEvent.click(input);
+
+ // Type 'op' which should match all options
+ await userEvent.type(input, 'op');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+
+ // Type 'opt' which should still match all options
+ await userEvent.type(input, 't');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+
+ // Type 'opti' which should match only 'Option 1'
+ await userEvent.type(input, 'i');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+ expect(screen.getByText('Option 1')).toBeInTheDocument();
+ });
+
+ it('should use default filter when autocomplete prop is false', async () => {
+ render();
+
+ // Open the dropdown
+ const input = screen.getByRole('combobox');
+ fireEvent.click(input);
+
+ // Type 'op' which should match all options
+ await userEvent.type(input, 'op');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+
+ // Type 'opt' which should still match all options
+ await userEvent.type(input, 't');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+
+ // Type 'opti' which should still match all options
+ await userEvent.type(input, 'i');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+
+ // Type 'option' which should still match all options
+ await userEvent.type(input, 'on');
+ expect(screen.getAllByRole('option')).toHaveLength(3);
+ });
+ });
});
diff --git a/packages/react/src/components/ComboBox/ComboBox.stories.js b/packages/react/src/components/ComboBox/ComboBox.stories.js
index 916f7777baae..7136b1e9c7b6 100644
--- a/packages/react/src/components/ComboBox/ComboBox.stories.js
+++ b/packages/react/src/components/ComboBox/ComboBox.stories.js
@@ -11,6 +11,7 @@ import { WithLayer } from '../../../.storybook/templates/WithLayer';
import ComboBox from '../ComboBox';
import mdx from './ComboBox.mdx';
+import { on } from 'process';
const items = [
{
@@ -92,6 +93,21 @@ export const AllowCustomValue = (args) => {
);
};
+export const AutocompleteWithTypeahead = (args) => {
+ return (
+
+
+
+ );
+};
export const ExperimentalAutoAlign = () => (
@@ -112,6 +128,10 @@ AllowCustomValue.argTypes = {
onChange: { action: 'onChange' },
};
+AutocompleteWithTypeahead.argTypes = {
+ onChange: { action: 'onChange' },
+};
+
export const _WithLayer = () => (
{(layer) => (
diff --git a/packages/react/src/components/ComboBox/ComboBox.tsx b/packages/react/src/components/ComboBox/ComboBox.tsx
index 9fdf344dd3e5..10cb9c11df74 100644
--- a/packages/react/src/components/ComboBox/ComboBox.tsx
+++ b/packages/react/src/components/ComboBox/ComboBox.tsx
@@ -159,6 +159,10 @@ export interface ComboBoxProps
*/
autoAlign?: boolean;
+ /**
+ * **Experimental**: will enable autcomplete and typeahead for the input field
+ */
+ autocomplete?: boolean;
/**
* An optional className to add to the container node
*/
@@ -323,6 +327,7 @@ const ComboBox = forwardRef(
['aria-label']: ariaLabel = 'Choose an item',
ariaLabel: deprecatedAriaLabel,
autoAlign = false,
+ autocomplete = false,
className: containerClassName,
direction = 'bottom',
disabled = false,
@@ -406,14 +411,32 @@ const ComboBox = forwardRef(
})
);
}
+ const autocompleteCustomFilter = (menu) => {
+ if (
+ !menu ||
+ typeof menu.item !== 'string' ||
+ typeof menu.inputValue !== 'string'
+ ) {
+ return false;
+ }
+ const item = menu.item.toLowerCase();
+ const input = menu.inputValue.toLowerCase();
+
+ if (input.length > item.length) {
+ return false;
+ }
+ return input.split('').every((char, index) => item[index] === char);
+ };
const filterItems = (
items: ItemType[],
itemToString: ItemToStringHandler,
inputValue: string | null
) =>
items.filter((item) =>
- shouldFilterItem
+ autocomplete
+ ? autocompleteCustomFilter({ item: itemToString(item), inputValue })
+ : shouldFilterItem
? shouldFilterItem({
item,
itemToString,
@@ -662,7 +685,13 @@ const ComboBox = forwardRef(
'aria-label': deprecatedAriaLabel || ariaLabel,
ref: autoAlign ? refs.setFloating : null,
}),
- [autoAlign, deprecatedAriaLabel, ariaLabel]
+ [
+ getMenuProps,
+ deprecatedAriaLabel,
+ ariaLabel,
+ autoAlign,
+ refs.setFloating,
+ ]
);
return (
@@ -884,6 +913,11 @@ ComboBox.propTypes = {
*/
autoAlign: PropTypes.bool,
+ /**
+ * **Experimental**: will enable autcomplete and typeahead for the input field
+ */
+ autocomplete: PropTypes.bool,
+
/**
* An optional className to add to the container node
*/
diff --git a/packages/react/src/components/ListBox/test-helpers.js b/packages/react/src/components/ListBox/test-helpers.js
index 4f83ea7966a6..e0c55700399e 100644
--- a/packages/react/src/components/ListBox/test-helpers.js
+++ b/packages/react/src/components/ListBox/test-helpers.js
@@ -7,7 +7,7 @@
const prefix = 'cds';
import userEvent from '@testing-library/user-event';
-import { act } from 'react';
+import { act } from '@testing-library/react';
// Finding nodes in a ListBox
export const findListBoxNode = () => {