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); +}