diff --git a/.changeset/violet-rice-appear.md b/.changeset/violet-rice-appear.md
new file mode 100644
index 00000000000..df22ae8e22f
--- /dev/null
+++ b/.changeset/violet-rice-appear.md
@@ -0,0 +1,5 @@
+---
+'@shopify/polaris': patch
+---
+
+`PopoverOverlay` closes focused `Popover` when pressing `Escape`
diff --git a/polaris-react/src/components/Combobox/tests/Combobox.test.tsx b/polaris-react/src/components/Combobox/tests/Combobox.test.tsx
index ad5e8c260d6..5e5c643b695 100644
--- a/polaris-react/src/components/Combobox/tests/Combobox.test.tsx
+++ b/polaris-react/src/components/Combobox/tests/Combobox.test.tsx
@@ -302,8 +302,14 @@ describe('', () => {
triggerFocus(combobox);
+ const target = combobox.find(TextField)!.find('input')!.domNode;
+ const keyupEvent = new KeyboardEvent('keyup', {
+ keyCode: Key.Escape,
+ bubbles: true,
+ });
+
combobox.act(() => {
- dispatchKeyup(Key.Escape);
+ target?.dispatchEvent(keyupEvent);
});
expect(combobox).toContainReactComponent(Popover, {
@@ -433,8 +439,3 @@ function triggerOptionSelected(combobox: any) {
}
function noop() {}
-
-function dispatchKeyup(key: Key) {
- const event: KeyboardEventInit & {keyCode: Key} = {keyCode: key};
- document.dispatchEvent(new KeyboardEvent('keyup', event));
-}
diff --git a/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx b/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx
index ff4576a44bb..4dce1ef97fd 100644
--- a/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx
+++ b/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx
@@ -299,8 +299,19 @@ export class PopoverOverlay extends PureComponent {
this.props.onClose(PopoverCloseSource.ScrollOut);
};
- private handleEscape = () => {
- this.props.onClose(PopoverCloseSource.EscapeKeypress);
+ private handleEscape = (event: Event) => {
+ const target = event.target as HTMLElement;
+ const {
+ contentNode,
+ props: {activator},
+ } = this;
+ const composedPath = event.composedPath();
+ const wasDescendant = wasContentNodeDescendant(composedPath, contentNode);
+ const isActivatorDescendant = nodeContainsDescendant(activator, target);
+
+ if (wasDescendant || isActivatorDescendant) {
+ this.props.onClose(PopoverCloseSource.EscapeKeypress);
+ }
};
private handleFocusFirstItem = () => {
diff --git a/polaris-react/src/components/Popover/components/PopoverOverlay/tests/PopoverOverlay.test.tsx b/polaris-react/src/components/Popover/components/PopoverOverlay/tests/PopoverOverlay.test.tsx
index a6bdb1ebc1a..7fbb429fd66 100644
--- a/polaris-react/src/components/Popover/components/PopoverOverlay/tests/PopoverOverlay.test.tsx
+++ b/polaris-react/src/components/Popover/components/PopoverOverlay/tests/PopoverOverlay.test.tsx
@@ -8,29 +8,10 @@ import {PositionedOverlay} from '../../../../PositionedOverlay';
import {PopoverOverlay} from '../PopoverOverlay';
import {Popover} from '../../../Popover';
-interface HandlerMap {
- [eventName: string]: (event: any) => void;
-}
-
-const listenerMap: HandlerMap = {};
-
describe('', () => {
- let addEventListener: jest.SpyInstance;
- let removeEventListener: jest.SpyInstance;
-
let rafSpy: jest.SpyInstance;
beforeEach(() => {
- addEventListener = jest.spyOn(document, 'addEventListener');
- addEventListener.mockImplementation((event, callback) => {
- listenerMap[event] = callback;
- });
-
- removeEventListener = jest.spyOn(document, 'removeEventListener');
- removeEventListener.mockImplementation((event) => {
- listenerMap[event] = noop;
- });
-
rafSpy = jest.spyOn(window, 'requestAnimationFrame');
rafSpy.mockImplementation((callback) => callback());
});
@@ -40,9 +21,6 @@ describe('', () => {
(document.activeElement as HTMLElement).blur();
}
- addEventListener.mockRestore();
- removeEventListener.mockRestore();
-
rafSpy.mockRestore();
});
@@ -204,24 +182,6 @@ describe('', () => {
});
});
- it('calls the onClose callback when the escape key is pressed', () => {
- const spy = jest.fn();
-
- mountWithApp(
-
- {children}
- ,
- );
-
- listenerMap.keyup({keyCode: Key.Escape});
- expect(spy).toHaveBeenCalledTimes(1);
- });
-
it('does not call the onClose callback when a descendent HTMLElement is clicked', () => {
const spy = jest.fn();
@@ -316,6 +276,77 @@ describe('', () => {
expect(spy).not.toHaveBeenCalled();
});
+ describe('when the Escape key is pressed', () => {
+ it('calls the onClose callback if event target is descendant', () => {
+ const spy = jest.fn();
+
+ const popoverOverlay = mountWithApp(
+
+ {}}
+ autoComplete="off"
+ />
+ ,
+ );
+
+ const target = popoverOverlay.find(TextField)!.find('input')!.domNode!;
+ triggerEscape(target);
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls the onClose callback if event target is activator descendant', () => {
+ const spy = jest.fn();
+
+ const popoverOverlay = mountWithApp(
+
+ {}}
+ autoComplete="off"
+ />
+ ,
+ );
+
+ const target = popoverOverlay.find(TextField)!.find('input')!.domNode!;
+ triggerEscape(target);
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not call the onClose callback if event target is not descendant or activator descendant', () => {
+ const spy = jest.fn();
+
+ mountWithApp(
+
+ {children}
+ ,
+ );
+
+ triggerEscape();
+
+ expect(spy).toHaveBeenCalledTimes(0);
+ });
+ });
+
describe('deleting descendant elements', () => {
const DeleteButton = () => {
const [show, setShow] = React.useState(true);
@@ -567,3 +598,12 @@ describe('', () => {
});
function noop() {}
+
+function triggerEscape(target?: Element) {
+ const keyupEvent = new KeyboardEvent('keyup', {
+ keyCode: Key.Escape,
+ bubbles: true,
+ });
+
+ (target || document).dispatchEvent(keyupEvent);
+}