Skip to content

Commit cadea8d

Browse files
authored
Fix MenuTrigger closing immediately (#2175)
1 parent 0e1b8f5 commit cadea8d

File tree

4 files changed

+41
-29
lines changed

4 files changed

+41
-29
lines changed

packages/@react-aria/interactions/src/useInteractOutside.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,23 @@ export function useInteractOutside(props: InteractOutsideProps) {
3333
let {ref, onInteractOutside, isDisabled, onInteractOutsideStart} = props;
3434
let stateRef = useRef({
3535
isPointerDown: false,
36-
ignoreEmulatedMouseEvents: false
36+
ignoreEmulatedMouseEvents: false,
37+
onInteractOutside,
38+
onInteractOutsideStart
3739
});
3840
let state = stateRef.current;
41+
state.onInteractOutside = onInteractOutside;
42+
state.onInteractOutsideStart = onInteractOutsideStart;
3943

4044
useEffect(() => {
45+
if (isDisabled) {
46+
return;
47+
}
48+
4149
let onPointerDown = (e) => {
42-
if (isDisabled) {
43-
return;
44-
}
45-
if (isValidEvent(e, ref) && onInteractOutside) {
46-
if (onInteractOutsideStart) {
47-
onInteractOutsideStart(e);
50+
if (isValidEvent(e, ref) && state.onInteractOutside) {
51+
if (state.onInteractOutsideStart) {
52+
state.onInteractOutsideStart(e);
4853
}
4954
state.isPointerDown = true;
5055
}
@@ -53,12 +58,9 @@ export function useInteractOutside(props: InteractOutsideProps) {
5358
// Use pointer events if available. Otherwise, fall back to mouse and touch events.
5459
if (typeof PointerEvent !== 'undefined') {
5560
let onPointerUp = (e) => {
56-
if (isDisabled) {
57-
return;
58-
}
59-
if (state.isPointerDown && onInteractOutside && isValidEvent(e, ref)) {
61+
if (state.isPointerDown && state.onInteractOutside && isValidEvent(e, ref)) {
6062
state.isPointerDown = false;
61-
onInteractOutside(e);
63+
state.onInteractOutside(e);
6264
}
6365
};
6466

@@ -72,25 +74,19 @@ export function useInteractOutside(props: InteractOutsideProps) {
7274
};
7375
} else {
7476
let onMouseUp = (e) => {
75-
if (isDisabled) {
76-
return;
77-
}
7877
if (state.ignoreEmulatedMouseEvents) {
7978
state.ignoreEmulatedMouseEvents = false;
80-
} else if (state.isPointerDown && onInteractOutside && isValidEvent(e, ref)) {
79+
} else if (state.isPointerDown && state.onInteractOutside && isValidEvent(e, ref)) {
8180
state.isPointerDown = false;
82-
onInteractOutside(e);
81+
state.onInteractOutside(e);
8382
}
8483
};
8584

8685
let onTouchEnd = (e) => {
87-
if (isDisabled) {
88-
return;
89-
}
9086
state.ignoreEmulatedMouseEvents = true;
91-
if (onInteractOutside && state.isPointerDown && isValidEvent(e, ref)) {
87+
if (state.onInteractOutside && state.isPointerDown && isValidEvent(e, ref)) {
9288
state.isPointerDown = false;
93-
onInteractOutside(e);
89+
state.onInteractOutside(e);
9490
}
9591
};
9692

@@ -106,7 +102,7 @@ export function useInteractOutside(props: InteractOutsideProps) {
106102
document.removeEventListener('touchend', onTouchEnd, true);
107103
};
108104
}
109-
}, [onInteractOutside, ref, state.ignoreEmulatedMouseEvents, state.isPointerDown, isDisabled]);
105+
}, [ref, state, isDisabled]);
110106
}
111107

112108
function isValidEvent(event, ref) {

packages/@react-spectrum/menu/src/MenuTrigger.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
8282
};
8383

8484
let contents = (
85-
<FocusScope restoreFocus>
85+
<FocusScope restoreFocus contain={isMobile}>
8686
<DismissButton onDismiss={state.close} />
8787
{menu}
8888
<DismissButton onDismiss={state.close} />
@@ -93,7 +93,7 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
9393
let overlay;
9494
if (isMobile) {
9595
overlay = (
96-
<Tray isOpen={state.isOpen} onClose={state.close} shouldCloseOnBlur>
96+
<Tray isOpen={state.isOpen} onClose={state.close}>
9797
{contents}
9898
</Tray>
9999
);

packages/@react-spectrum/picker/src/Picker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function Picker<T extends object>(props: SpectrumPickerProps<T>, ref: DOMRef<HTM
117117

118118
// On small screen devices, the listbox is rendered in a tray, otherwise a popover.
119119
let listbox = (
120-
<FocusScope restoreFocus>
120+
<FocusScope restoreFocus contain={isMobile}>
121121
<DismissButton onDismiss={() => state.close()} />
122122
<ListBoxBase
123123
{...menuProps}
@@ -158,7 +158,7 @@ function Picker<T extends object>(props: SpectrumPickerProps<T>, ref: DOMRef<HTM
158158
let overlay;
159159
if (isMobile) {
160160
overlay = (
161-
<Tray isOpen={state.isOpen} onClose={state.close} shouldCloseOnBlur>
161+
<Tray isOpen={state.isOpen} onClose={state.close}>
162162
{listbox}
163163
</Tray>
164164
);

packages/@react-spectrum/table/test/Table.test.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ describe('TableView', function () {
9191
beforeAll(function () {
9292
offsetWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000);
9393
offsetHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 1000);
94-
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
9594
jest.useFakeTimers();
9695
});
9796

@@ -100,6 +99,10 @@ describe('TableView', function () {
10099
offsetHeight.mockReset();
101100
});
102101

102+
beforeEach(() => {
103+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
104+
});
105+
103106
afterEach(() => {
104107
act(() => {jest.runAllTimers();});
105108
});
@@ -2628,13 +2631,18 @@ describe('TableView', function () {
26282631
let menuItems = within(menu).getAllByRole('menuitem');
26292632
expect(menuItems.length).toBe(2);
26302633

2634+
// Need requestAnimationFrame to actually be async for this test to work.
2635+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 0));
2636+
26312637
triggerPress(menuItems[1]);
2638+
act(() => jest.runAllTimers());
26322639
expect(menu).not.toBeInTheDocument();
26332640

26342641
let dialog = tree.getByRole('alertdialog', {hidden: true});
26352642
let deleteButton = within(dialog).getByRole('button', {hidden: true});
26362643

26372644
triggerPress(deleteButton);
2645+
act(() => jest.runAllTimers());
26382646
expect(dialog).not.toBeInTheDocument();
26392647

26402648
act(() => jest.runAllTimers());
@@ -2828,15 +2836,23 @@ describe('TableView', function () {
28282836
expect(document.activeElement).toBe(within(rows[1]).getByRole('button'));
28292837

28302838
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', altKey: true});
2839+
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown', altKey: true});
28312840

28322841
let menu = tree.getByRole('menu');
28332842
expect(menu).toBeInTheDocument();
28342843
expect(document.activeElement).toBe(within(menu).getAllByRole('menuitem')[0]);
28352844

28362845
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
2846+
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
28372847
expect(document.activeElement).toBe(within(menu).getAllByRole('menuitem')[1]);
28382848

2839-
act(() => table.focus());
2849+
// Need requestAnimationFrame to actually be async for this test to work.
2850+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 0));
2851+
2852+
fireEvent.keyDown(document.activeElement, {key: 'Escape'});
2853+
fireEvent.keyUp(document.activeElement, {key: 'Escape'});
2854+
2855+
act(() => jest.runAllTimers());
28402856

28412857
expect(menu).not.toBeInTheDocument();
28422858
expect(document.activeElement).toBe(within(rows[1]).getByRole('button'));

0 commit comments

Comments
 (0)