Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/empty-beans-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": minor
---

Add Picker component as a more advanced version of Select.
6 changes: 3 additions & 3 deletions src/components/fields/ComboBox/ComboBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { userEvent, within } from 'storybook/test';

import { baseProps } from '../../../stories/lists/baseProps';
Expand Down Expand Up @@ -772,13 +772,13 @@ export const ShowAllOnNoResults: StoryObj<typeof ComboBox> = {
};

export const VirtualizedList = () => {
const [selected, setSelected] = useState<string | null>(null);

interface Item {
key: string;
name: string;
}

const [selected, setSelected] = useState<string | null>(null);

// Generate a large list of items with varying content to test virtualization
const items: Item[] = Array.from({ length: 1000 }, (_, i) => ({
key: `item-${i}`,
Expand Down
135 changes: 133 additions & 2 deletions src/components/fields/ComboBox/ComboBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ describe('<ComboBox />', () => {
});
});

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

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

onSelectionChange.mockClear();

// Type invalid text to make input invalid
await userEvent.clear(combobox);
await userEvent.type(combobox, 'xyz');

// Blur the input
await act(async () => {
combobox.blur();
});

// Should clear selection on blur
// Should clear selection on blur because input is invalid
await waitFor(() => {
expect(onSelectionChange).toHaveBeenCalledWith(null);
expect(combobox).toHaveValue('');
Expand Down Expand Up @@ -585,6 +591,131 @@ describe('<ComboBox />', () => {
});
});

it('should auto-select when there is exactly one filtered result on blur', async () => {
const onSelectionChange = jest.fn();

const { getByRole } = renderWithRoot(
<ComboBox label="test" onSelectionChange={onSelectionChange}>
{items.map((item) => (
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
))}
</ComboBox>,
);

const combobox = getByRole('combobox');

// Type partial match that results in one item (Violet is unique with 'vio')
await userEvent.type(combobox, 'vio');

// Blur the input
await act(async () => {
combobox.blur();
});

// Should auto-select the single matching item
await waitFor(() => {
expect(onSelectionChange).toHaveBeenCalledWith('violet');
expect(combobox).toHaveValue('Violet');
});
});

it('should reset to selected value on blur when clearOnBlur is false and input is invalid', async () => {
const onSelectionChange = jest.fn();

const { getByRole, getAllByRole, queryByRole } = renderWithRoot(
<ComboBox label="test" onSelectionChange={onSelectionChange}>
{items.map((item) => (
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
))}
</ComboBox>,
);

const combobox = getByRole('combobox');

// Type to filter and open popover
await userEvent.type(combobox, 're');

await waitFor(() => {
expect(queryByRole('listbox')).toBeInTheDocument();
});

// Click on first option (Red)
const options = getAllByRole('option');
await userEvent.click(options[0]);

// Verify selection was made
await waitFor(() => {
expect(onSelectionChange).toHaveBeenCalledWith('red');
expect(combobox).toHaveValue('Red');
});

onSelectionChange.mockClear();

// Type invalid text to make input invalid
await userEvent.clear(combobox);
await userEvent.type(combobox, 'xyz');

// Blur the input
await act(async () => {
combobox.blur();
});

// Should reset input to selected value (Red) since clearOnBlur is false
await waitFor(() => {
expect(combobox).toHaveValue('Red');
});

// Selection should not change
expect(onSelectionChange).not.toHaveBeenCalled();
});

it('should clear selection when input is empty on blur even with clearOnBlur false', async () => {
const onSelectionChange = jest.fn();

const { getByRole, getAllByRole, queryByRole } = renderWithRoot(
<ComboBox label="test" onSelectionChange={onSelectionChange}>
{items.map((item) => (
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
))}
</ComboBox>,
);

const combobox = getByRole('combobox');

// Type to filter and open popover
await userEvent.type(combobox, 're');

await waitFor(() => {
expect(queryByRole('listbox')).toBeInTheDocument();
});

// Click on first option (Red)
const options = getAllByRole('option');
await userEvent.click(options[0]);

// Verify selection was made
await waitFor(() => {
expect(onSelectionChange).toHaveBeenCalledWith('red');
expect(combobox).toHaveValue('Red');
});

onSelectionChange.mockClear();

// Clear the input completely
await userEvent.clear(combobox);

// Blur the input
await act(async () => {
combobox.blur();
});

// Should clear selection even though clearOnBlur is false
await waitFor(() => {
expect(onSelectionChange).toHaveBeenCalledWith(null);
expect(combobox).toHaveValue('');
});
});

it('should show all items when opening with no results', async () => {
const { getByRole, getAllByRole, queryByRole, getByTestId } =
renderWithRoot(
Expand Down
Loading
Loading