Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: combobox's new feature, autocomplete with typeahead #17268

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,9 @@ Map {
"translateWithId": Object {
"type": "func",
},
"typeahead": Object {
"type": "bool",
},
"warn": Object {
"type": "bool",
},
Expand Down
147 changes: 145 additions & 2 deletions packages/react/src/components/ComboBox/ComboBox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AILabel } from '../AILabel';

const findInputNode = () => screen.getByRole('combobox');
const openMenu = async () => {
await userEvent.click(screen.getByTitle('Open'));
await userEvent.click(screen.getByRole('combobox'));
};

const prefix = 'cds';
Expand Down Expand Up @@ -456,7 +456,7 @@ describe('ComboBox', () => {
render(<ComboBox {...mockProps} allowCustomValue={false} />);
await userEvent.type(findInputNode(), '1');
expect(screen.getAllByRole('option')[1]).toHaveClass(
'cds--list-box__menu-item--highlighted'
'cds--list-box__menu-item'
);
});

Expand Down Expand Up @@ -497,4 +497,147 @@ 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' },
{ id: 'apple', text: 'Apple' },
{ id: 'banana', text: 'Banana' },
{ id: 'orange', text: 'Orange' },
{ id: 'orangeish', text: 'Orangeish' },
];

const mockProps = {
id: 'test-combobox',
items,
itemToString: (item) => (item ? item.text : ''),
onChange: jest.fn(),
};

it('should respect autocomplete prop', async () => {
render(<ComboBox {...mockProps} typeahead />);
await waitForPosition();
const inputNode = findInputNode();
expect(inputNode).toHaveAttribute('autocomplete');
});
it('should use autocompleteCustomFilter when autocomplete prop is true', async () => {
const user = userEvent.setup();
render(<ComboBox {...mockProps} typeahead />);

// Open the dropdown
const input = screen.getByRole('combobox');
user.click(input);

// Type 'op' which should match all options
await user.type(input, 'op');
expect(screen.getAllByRole('option')).toHaveLength(3);

// Type 'opt' which should still match all options
await user.type(input, 't');
expect(screen.getAllByRole('option')).toHaveLength(3);

// Type 'opti' which should match only 'Option 1'
await user.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 () => {
const user = userEvent.setup();
render(<ComboBox {...mockProps} />);

// Open the dropdown
const input = screen.getByRole('combobox');
user.click(input);

// Type 'op' which should match all options
await user.type(input, 'op');
expect(screen.getAllByRole('option')).toHaveLength(7);

// Type 'opt' which should still match all options
await user.type(input, 't');
expect(screen.getAllByRole('option')).toHaveLength(7);

// Type 'opti' which should still match all options
await user.type(input, 'i');
expect(screen.getAllByRole('option')).toHaveLength(7);

// Type 'option' which should still match all options
await user.type(input, 'on');
expect(screen.getAllByRole('option')).toHaveLength(7);
});

it('should not autocomplete when no match is found', async () => {
const user = userEvent.setup();
render(<ComboBox {...mockProps} typeahead />);

const input = screen.getByRole('combobox');
user.click(input);

await user.type(input, 'xyz');
await user.keyboard('[Tab]');

expect(document.activeElement).not.toBe(input);
});
it('should suggest best matching typeahread suggestion and complete it in Tab key press', async () => {
const user = userEvent.setup();
render(<ComboBox {...mockProps} typeahead />);

// Open the dropdown
const input = screen.getByRole('combobox');
user.click(input);

// Type 'op' which should match all options
await user.type(input, 'Ap');

await user.keyboard('[Tab]');

expect(findInputNode()).toHaveDisplayValue('Apple');
});
it('should not autocomplete on Tab after backspace', async () => {
const user = userEvent.setup();
render(<ComboBox {...mockProps} allowCustomValue typeahead />);

const input = screen.getByRole('combobox');
user.click(input);

await user.type(input, 'App');
await user.keyboard('[Backspace]');

await user.keyboard('[Tab]');

expect(document.activeElement).not.toBe(input);
});
it('should autocomplete with the first matching suggestion when multiple matches exist', async () => {
const multipleMatchProps = {
...mockProps,
options: ['Apple', 'Application', 'Apricot'],
};
const user = userEvent.setup();
render(<ComboBox {...multipleMatchProps} allowCustomValue typeahead />);

const input = screen.getByRole('combobox');
user.click(input);

await user.type(input, 'App');
await user.keyboard('[Tab]');

expect(input).toHaveDisplayValue('Apple');
});

it('should match case exactly with option list when Tab is pressed', async () => {
const user = userEvent.setup();
render(<ComboBox {...mockProps} allowCustomValue typeahead />);

const input = screen.getByRole('combobox');
user.click(input);

await user.type(input, 'APpl');
await user.keyboard('[Tab]');

expect(input).toHaveDisplayValue('Apple');
});
});
});
20 changes: 20 additions & 0 deletions packages/react/src/components/ComboBox/ComboBox.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ export const AllowCustomValue = (args) => {
</div>
);
};

export const AutocompleteWithTypeahead = (args) => {
return (
<div style={{ width: 300 }}>
<ComboBox
allowCustomValue
onChange={args.onChange}
helperText="Combobox helper text"
id="carbon-combobox"
items={['Apple', 'Orange', 'Banana', 'Pineapple', 'Raspberry', 'Lime']}
titleText="ComboBox title"
typeahead
/>
</div>
);
};
export const ExperimentalAutoAlign = () => (
<div style={{ width: 400 }}>
<div style={{ height: 300 }}></div>
Expand Down Expand Up @@ -273,6 +289,10 @@ export const Playground = (args) => (
</div>
);

AutocompleteWithTypeahead.argTypes = {
onChange: { action: 'onChange' },
};

Playground.argTypes = {
['aria-label']: {
table: {
Expand Down
Loading
Loading