Skip to content

Commit 1e910c0

Browse files
committed
fix(ComboBox): filtering optimizations
1 parent cd8cf48 commit 1e910c0

File tree

3 files changed

+328
-114
lines changed

3 files changed

+328
-114
lines changed

src/components/fields/ComboBox/ComboBox.test.tsx

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ describe('<ComboBox />', () => {
463463
});
464464
});
465465

466-
it('should clear selection on blur when clearOnBlur is true', async () => {
466+
it('should clear invalid input on blur when clearOnBlur is true', async () => {
467467
const onSelectionChange = jest.fn();
468468

469469
const { getByRole, getAllByRole, queryByRole } = renderWithRoot(
@@ -493,12 +493,18 @@ describe('<ComboBox />', () => {
493493
expect(combobox).toHaveValue('Red');
494494
});
495495

496+
onSelectionChange.mockClear();
497+
498+
// Type invalid text to make input invalid
499+
await userEvent.clear(combobox);
500+
await userEvent.type(combobox, 'xyz');
501+
496502
// Blur the input
497503
await act(async () => {
498504
combobox.blur();
499505
});
500506

501-
// Should clear selection on blur
507+
// Should clear selection on blur because input is invalid
502508
await waitFor(() => {
503509
expect(onSelectionChange).toHaveBeenCalledWith(null);
504510
expect(combobox).toHaveValue('');
@@ -585,6 +591,131 @@ describe('<ComboBox />', () => {
585591
});
586592
});
587593

594+
it('should auto-select when there is exactly one filtered result on blur', async () => {
595+
const onSelectionChange = jest.fn();
596+
597+
const { getByRole } = renderWithRoot(
598+
<ComboBox label="test" onSelectionChange={onSelectionChange}>
599+
{items.map((item) => (
600+
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
601+
))}
602+
</ComboBox>,
603+
);
604+
605+
const combobox = getByRole('combobox');
606+
607+
// Type partial match that results in one item (Violet is unique with 'vio')
608+
await userEvent.type(combobox, 'vio');
609+
610+
// Blur the input
611+
await act(async () => {
612+
combobox.blur();
613+
});
614+
615+
// Should auto-select the single matching item
616+
await waitFor(() => {
617+
expect(onSelectionChange).toHaveBeenCalledWith('violet');
618+
expect(combobox).toHaveValue('Violet');
619+
});
620+
});
621+
622+
it('should reset to selected value on blur when clearOnBlur is false and input is invalid', async () => {
623+
const onSelectionChange = jest.fn();
624+
625+
const { getByRole, getAllByRole, queryByRole } = renderWithRoot(
626+
<ComboBox label="test" onSelectionChange={onSelectionChange}>
627+
{items.map((item) => (
628+
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
629+
))}
630+
</ComboBox>,
631+
);
632+
633+
const combobox = getByRole('combobox');
634+
635+
// Type to filter and open popover
636+
await userEvent.type(combobox, 're');
637+
638+
await waitFor(() => {
639+
expect(queryByRole('listbox')).toBeInTheDocument();
640+
});
641+
642+
// Click on first option (Red)
643+
const options = getAllByRole('option');
644+
await userEvent.click(options[0]);
645+
646+
// Verify selection was made
647+
await waitFor(() => {
648+
expect(onSelectionChange).toHaveBeenCalledWith('red');
649+
expect(combobox).toHaveValue('Red');
650+
});
651+
652+
onSelectionChange.mockClear();
653+
654+
// Type invalid text to make input invalid
655+
await userEvent.clear(combobox);
656+
await userEvent.type(combobox, 'xyz');
657+
658+
// Blur the input
659+
await act(async () => {
660+
combobox.blur();
661+
});
662+
663+
// Should reset input to selected value (Red) since clearOnBlur is false
664+
await waitFor(() => {
665+
expect(combobox).toHaveValue('Red');
666+
});
667+
668+
// Selection should not change
669+
expect(onSelectionChange).not.toHaveBeenCalled();
670+
});
671+
672+
it('should clear selection when input is empty on blur even with clearOnBlur false', async () => {
673+
const onSelectionChange = jest.fn();
674+
675+
const { getByRole, getAllByRole, queryByRole } = renderWithRoot(
676+
<ComboBox label="test" onSelectionChange={onSelectionChange}>
677+
{items.map((item) => (
678+
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
679+
))}
680+
</ComboBox>,
681+
);
682+
683+
const combobox = getByRole('combobox');
684+
685+
// Type to filter and open popover
686+
await userEvent.type(combobox, 're');
687+
688+
await waitFor(() => {
689+
expect(queryByRole('listbox')).toBeInTheDocument();
690+
});
691+
692+
// Click on first option (Red)
693+
const options = getAllByRole('option');
694+
await userEvent.click(options[0]);
695+
696+
// Verify selection was made
697+
await waitFor(() => {
698+
expect(onSelectionChange).toHaveBeenCalledWith('red');
699+
expect(combobox).toHaveValue('Red');
700+
});
701+
702+
onSelectionChange.mockClear();
703+
704+
// Clear the input completely
705+
await userEvent.clear(combobox);
706+
707+
// Blur the input
708+
await act(async () => {
709+
combobox.blur();
710+
});
711+
712+
// Should clear selection even though clearOnBlur is false
713+
await waitFor(() => {
714+
expect(onSelectionChange).toHaveBeenCalledWith(null);
715+
expect(combobox).toHaveValue('');
716+
});
717+
});
718+
588719
it('should show all items when opening with no results', async () => {
589720
const { getByRole, getAllByRole, queryByRole, getByTestId } =
590721
renderWithRoot(

0 commit comments

Comments
 (0)