From 4b255bba9bea97ceaf4006cab7dddad16de1ff18 Mon Sep 17 00:00:00 2001 From: Andrew Musgrave Date: Mon, 7 Oct 2019 17:27:05 -0400 Subject: [PATCH] Fixed Popover focusing incorrect element when closed --- src/components/Popover/Popover.tsx | 33 ++++- src/components/Popover/tests/Popover.test.tsx | 43 ++++++ src/components/Portal/Portal.tsx | 3 +- src/components/Portal/tests/Portal.test.tsx | 5 +- src/components/shared.ts | 5 + src/utilities/focus.ts | 37 +++++ src/utilities/is-element-in-viewport.ts | 10 ++ src/utilities/tests/focus.test.ts | 134 ++++++++++++++++-- .../tests/is-element-in-viewport.test.ts | 73 ++++++++++ 9 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 src/utilities/is-element-in-viewport.ts create mode 100644 src/utilities/tests/is-element-in-viewport.test.ts diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx index 2d06a55dcb4..b2b452506bb 100644 --- a/src/components/Popover/Popover.tsx +++ b/src/components/Popover/Popover.tsx @@ -1,12 +1,11 @@ import React from 'react'; import {createUniqueIDFactory} from '@shopify/javascript-utilities/other'; -import { - focusFirstFocusableNode, - findFirstFocusableNode, -} from '@shopify/javascript-utilities/focus'; +import {findFirstFocusableNode} from '@shopify/javascript-utilities/focus'; +import {focusNextFocusableNode} from '../../utilities/focus'; import {PreferredPosition, PreferredAlignment} from '../PositionedOverlay'; import {Portal} from '../Portal'; +import {portal} from '../shared'; import {CloseSource, Pane, PopoverOverlay, Section} from './components'; export {CloseSource}; @@ -126,16 +125,25 @@ export class Popover extends React.PureComponent { } private handleClose = (source: CloseSource) => { + const {activatorNode} = this.state; this.props.onClose(source); if (this.activatorContainer == null) { return; } + if ( - source === CloseSource.FocusOut || - source === CloseSource.EscapeKeypress + (source === CloseSource.FocusOut || + source === CloseSource.EscapeKeypress) && + activatorNode ) { - focusFirstFocusableNode(this.activatorContainer, false); + const focusableActivator = + findFirstFocusableNode(activatorNode) || + findFirstFocusableNode(this.activatorContainer) || + this.activatorContainer; + if (!focusNextFocusableNode(focusableActivator, isInPortal)) { + focusableActivator.focus(); + } } }; @@ -150,3 +158,14 @@ export class Popover extends React.PureComponent { this.activatorContainer = node; }; } + +function isInPortal(element: Element) { + let parentElement = element.parentElement; + + while (parentElement) { + if (parentElement.matches(portal.selector)) return false; + parentElement = parentElement.parentElement; + } + + return true; +} diff --git a/src/components/Popover/tests/Popover.test.tsx b/src/components/Popover/tests/Popover.test.tsx index 5f930bbeef8..13daf3e5862 100644 --- a/src/components/Popover/tests/Popover.test.tsx +++ b/src/components/Popover/tests/Popover.test.tsx @@ -1,6 +1,8 @@ import React, {useState, useCallback} from 'react'; import {mountWithAppProvider, findByTestID} from 'test-utilities/legacy'; +import {mountWithApp} from 'test-utilities'; import {Popover} from '../Popover'; +import {PopoverOverlay} from '../components'; describe('', () => { const spy = jest.fn(); @@ -173,4 +175,45 @@ describe('', () => { expect(onCloseSpy).not.toHaveBeenCalled(); }); + + it('focuses the next available element when the popover is closed', () => { + const id = 'focus-target'; + function PopoverTest() { + return ( + +
+ } onClose={noop} /> +
+