Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
16 changes: 15 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,19 @@ 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) => {
if (nodeContains(buttonRef?.current, getEventTarget(e) as Element)) {
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();
});
});
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