Skip to content

Commit

Permalink
ComboBox controlled state behavior bug fix (#2048)
Browse files Browse the repository at this point in the history
Co-authored-by: Rohan Kadkol <45748283+rohan-kadkol@users.noreply.github.com>
Co-authored-by: Mayank <mayank99@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 13, 2024
1 parent 3595174 commit 9277c26
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 291 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-impalas-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/itwinui-react": patch
---

Fixed a bug in `ComboBox` where the `isSelected` passed to `itemRenderer` was always `false` whenever `multiple` was `true`.
9 changes: 9 additions & 0 deletions .changeset/happy-mirrors-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@itwin/itwinui-react": minor
---

Fixed a bug in `ComboBox` where the controlled state (`value` prop) was not given priority over the uncontrolled state.

As a result:
* Setting the default value using `value={myDefaultValue}` will no longer work. Instead, use the new `defaultValue` prop.
* Resetting the value using `value={null}` will now force the ComboBox to be in *controlled* mode. If you want to reset the value but be in *uncontrolled* mode, then use `value={undefined}` instead.
5 changes: 5 additions & 0 deletions .changeset/six-buckets-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/itwinui-react": minor
---

Added a new `defaultValue` prop to `ComboBox`. This is useful when you don't want to maintain your own state but still want to control the initial `value`.
47 changes: 39 additions & 8 deletions packages/itwinui-react/src/core/ComboBox/ComboBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,12 @@ it('should accept inputProps', () => {
);
});

it('should use the defaultValue when passed', () => {
const { container } = renderComponent({ defaultValue: 1 });
const input = assertBaseElement(container);
expect(input).toHaveValue('Item 1');
});

it('should work with custom itemRenderer', async () => {
const mockOnChange = vi.fn();
const { container, getByText } = renderComponent({
Expand Down Expand Up @@ -688,24 +694,49 @@ it('should accept ReactNode in emptyStateMessage', async () => {
});

it('should programmatically clear value', async () => {
const mockOnChange = vi.fn();
const options = [0, 1, 2].map((value) => ({ value, label: `Item ${value}` }));

const { container, rerender } = render(
<ComboBox options={options} value={1} />,
);

const input = container.querySelector('.iui-input') as HTMLInputElement;
expect(input).toHaveValue('Item 1');

rerender(<ComboBox options={options} value={undefined} />);

// value={undefined} = reset value + uncontrolled state.
expect(input).toHaveValue(''); // should be reset

await userEvent.tab();
await userEvent.click(screen.getByText('Item 2'));
expect(input).toHaveValue('Item 2'); // uncontrolled state should work

rerender(<ComboBox options={options} value={null} />);

// value={null} = reset value + controlled state.
expect(input).toHaveValue(''); // should be reset

await userEvent.click(input);
await userEvent.click(screen.getByText('Item 0'));
expect(input).toHaveValue(''); // should not change since controlled state
});

it('should respect controlled state', async () => {
const mockOnChange = vi.fn();
const options = [0, 1, 2].map((value) => ({ value, label: `Item ${value}` }));

const { container } = render(
<ComboBox options={options} onChange={mockOnChange} value={1} />,
);

await userEvent.tab();
await userEvent.click(screen.getByText('Item 2'));
const input = container.querySelector('.iui-input') as HTMLInputElement;
expect(mockOnChange).toHaveBeenCalledWith(2);
expect(input).toHaveValue('Item 2');

rerender(
<ComboBox options={options} onChange={mockOnChange} value={undefined} />,
);
expect(mockOnChange).not.toHaveBeenCalledTimes(2);
expect(input).toHaveValue('');
// In controlled state, passed value should take priority
expect(input).toHaveValue('Item 1');
});

it('should update options (have selected option in new options list)', async () => {
Expand Down Expand Up @@ -1010,7 +1041,7 @@ it('should update live region when selection changes', async () => {
<ComboBox
options={[0, 1, 2].map((value) => ({ value, label: `Item ${value}` }))}
multiple
value={[0]}
defaultValue={[0]}
/>,
);

Expand Down
Loading

0 comments on commit 9277c26

Please sign in to comment.