Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/@react-aria/combobox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@react-aria/focus": "^3.21.4",
"@react-aria/i18n": "^3.12.15",
"@react-aria/interactions": "^3.27.0",
"@react-aria/listbox": "^3.15.2",
"@react-aria/live-announcer": "^3.4.4",
"@react-aria/menu": "^3.20.0",
Expand Down
17 changes: 16 additions & 1 deletion packages/@react-aria/combobox/src/useComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {getChildNodes, getItemCount} from '@react-stately/collections';
import intlMessages from '../intl/*.json';
import {ListKeyboardDelegate, useSelectableCollection} from '@react-aria/selection';
import {privateValidationStateProp} from '@react-stately/form';
import {useInteractOutside} from '@react-aria/interactions';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useMenuTrigger} from '@react-aria/menu';
import {useTextField} from '@react-aria/textfield';
Expand Down Expand Up @@ -225,7 +226,7 @@ export function useComboBox<T, M extends SelectionMode = 'single'>(props: AriaCo
}, inputRef);

useFormReset(inputRef, state.defaultValue, state.setValue);

// Press handlers for the ComboBox button
let onPress = (e: PressEvent) => {
if (e.pointerType === 'touch') {
Expand Down Expand Up @@ -365,6 +366,20 @@ export function useComboBox<T, M extends SelectionMode = 'single'>(props: AriaCo
state.close();
} : undefined);

// usePopover -> useOverlay calls useInteractOutside, but ComboBox is non-modal, so `isDismissable` is false
// Because of this, onInteractOutside is not passed to useInteractOutside, so we need to call it here.
useInteractOutside({
ref: popoverRef,
onInteractOutside: (e) => {
let target = getEventTarget(e) as Element;
if (nodeContains(buttonRef?.current, target) || nodeContains(inputRef.current, target)) {
return;
}
state.close();
},
isDisabled: !state.isOpen
});

return {
labelProps,
buttonProps: {
Expand Down
24 changes: 23 additions & 1 deletion packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {Avatar, Button, ComboBox, ComboBoxItem, ComboBoxSection, Content, ContextualHelp, Footer, Form, Header, Heading, Link, Text} from '../src';
import {Avatar, Button, ComboBox, ComboBoxItem, ComboBoxSection, Content, ContextualHelp, Dialog, DialogTrigger, Footer, Form, Header, Heading, Link, Text} from '../src';
import {categorizeArgTypes, getActionArgs} from './utils';
import {ComboBoxProps} from 'react-aria-components';
import DeviceDesktopIcon from '../s2wf-icons/S2_Icon_DeviceDesktop_20_N.svg';
Expand Down Expand Up @@ -362,3 +362,25 @@ export function WithCreateOption() {
</ComboBox>
);
}

export const ComboboxInsideDialog: Story = {
render: (args) => (
<DialogTrigger>
<Button>Open</Button>
<Dialog isDismissible>
<Heading>Combo Box in a Dialog</Heading>
<Content>
<ComboBox {...args}>
<ComboBoxItem>Aardvark</ComboBoxItem>
<ComboBoxItem>Cat</ComboBoxItem>
<ComboBoxItem>Dog</ComboBoxItem>
<ComboBoxItem>Kangaroo</ComboBoxItem>
<ComboBoxItem>Panda</ComboBoxItem>
<ComboBoxItem>Snake</ComboBoxItem>
</ComboBox>
</Content>
</Dialog>
</DialogTrigger>
),
args: Example.args
};
55 changes: 53 additions & 2 deletions packages/@react-spectrum/s2/test/Combobox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
*/

jest.mock('@react-aria/live-announcer');
import {act, pointerMap, render, setupIntersectionObserverMock, within} from '@react-spectrum/test-utils-internal';
import {act, fireEvent, pointerMap, render, setupIntersectionObserverMock, within} from '@react-spectrum/test-utils-internal';
import {announce} from '@react-aria/live-announcer';
import {ComboBox, ComboBoxItem, Content, ContextualHelp, Heading, Text} from '../src';
import {Button, ComboBox, ComboBoxItem, Content, ContextualHelp, Dialog, DialogTrigger, Heading, Text} from '../src';
import React from 'react';
import {User} from '@react-aria/test-utils';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -213,4 +213,55 @@ describe('Combobox', () => {
expect(tree.getAllByText('Contents')[1]).toBeVisible();
warn.mockRestore();
});

it('should close the combobox when clicking outside the combobox on a dialog backdrop', async () => {
let tree = render(
<DialogTrigger>
<Button>Open</Button>
<Dialog isDismissible>
<Heading>Combo Box in a Dialog</Heading>
<Content>
<ComboBox label="test">
<ComboBoxItem>Aardvark</ComboBoxItem>
<ComboBoxItem>Cat</ComboBoxItem>
<ComboBoxItem>Dog</ComboBoxItem>
<ComboBoxItem>Kangaroo</ComboBoxItem>
<ComboBoxItem>Panda</ComboBoxItem>
<ComboBoxItem>Snake</ComboBoxItem>
</ComboBox>
</Content>
</Dialog>
</DialogTrigger>
);

let dialogTester = testUtilUser.createTester('Dialog', {root: tree.container, interactionType: 'mouse'});
await dialogTester.open();
expect(dialogTester.dialog).toBeVisible();
act(() => {
jest.runAllTimers();
});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: dialogTester.dialog!, interactionType: 'mouse'});
await comboboxTester.open();

expect(comboboxTester.listbox).toBeVisible();
act(() => {
jest.runAllTimers();
});
let backdrop = document.querySelector('[style*="--visual-viewport-height"]');
// can't use userEvent here for some reason
fireEvent.mouseDown(backdrop!, {button: 0});
fireEvent.mouseUp(backdrop!, {button: 0});
act(() => {
jest.runAllTimers();
});
expect(comboboxTester.listbox).toBeNull();


fireEvent.mouseDown(backdrop!, {button: 0});
fireEvent.mouseUp(backdrop!, {button: 0});
act(() => {
jest.runAllTimers();
});
expect(dialogTester.dialog).toBeNull();
});
});
52 changes: 52 additions & 0 deletions packages/react-aria-components/test/ComboBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,4 +761,56 @@ describe('ComboBox', () => {
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenLastCalledWith(['1']);
});

it('should not close the combobox when clicking on the input', async () => {
let onOpenChange = jest.fn();
let {container, getByRole} = render(<TestComboBox onOpenChange={onOpenChange} />);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
await comboboxTester.open();
act(() => {
jest.runAllTimers();
});
expect(comboboxTester.listbox).toBeVisible();
expect(comboboxTester.combobox).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();

await user.click(getByRole('combobox'));
act(() => {
jest.runAllTimers();
});
expect(comboboxTester.listbox).toBeVisible();
expect(comboboxTester.combobox).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(0);
});

it('should close the combobox when clicking on the button, and it should reopen if clicked again', async () => {
let onOpenChange = jest.fn();
let {container, getByRole, getAllByRole} = render(<TestComboBox onOpenChange={onOpenChange} />);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
await comboboxTester.open();
act(() => {
jest.runAllTimers();
});
expect(comboboxTester.listbox).toBeVisible();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();

await user.click(getAllByRole('button', {hidden: true})[0]);
act(() => {
jest.runAllTimers();
});
expect(comboboxTester.listbox).toBeNull();
expect(comboboxTester.combobox).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();

await user.click(getByRole('button'));
act(() => {
jest.runAllTimers();
});
expect(comboboxTester.listbox).toBeVisible();
expect(comboboxTester.combobox).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5539,6 +5539,7 @@ __metadata:
dependencies:
"@react-aria/focus": "npm:^3.21.4"
"@react-aria/i18n": "npm:^3.12.15"
"@react-aria/interactions": "npm:^3.27.0"
"@react-aria/listbox": "npm:^3.15.2"
"@react-aria/live-announcer": "npm:^3.4.4"
"@react-aria/menu": "npm:^3.20.0"
Expand Down