-
Notifications
You must be signed in to change notification settings - Fork 829
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[EuiPopover][EuiContextMenu] Attempt to manually restore focus to the toggling button on popover close if focus becomes stranded #5760
Changes from 4 commits
b60e810
8eb8105
ac56282
a429a4c
ae21121
254dc79
75342b4
c01d2f4
39e49e4
148ddb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -439,6 +439,74 @@ describe('EuiPopover', () => { | |
jest.advanceTimersByTime(10); | ||
}); | ||
}); | ||
|
||
describe('refocusButtonOnClose', () => { | ||
let rafSpy: jest.SpyInstance; | ||
beforeAll(() => { | ||
rafSpy = jest | ||
.spyOn(window, 'requestAnimationFrame') | ||
.mockImplementation((cb: Function) => cb()); | ||
}); | ||
afterAll(() => { | ||
rafSpy.mockRestore(); | ||
}); | ||
|
||
it('refocuses the toggle button on focus trap deactivation', () => { | ||
const toggleButtonEl = React.createRef<HTMLButtonElement>(); | ||
const toggleButton = <button ref={toggleButtonEl} />; | ||
|
||
const component = mount( | ||
<EuiPopover | ||
button={toggleButton} | ||
isOpen={true} | ||
closePopover={() => {}} | ||
{...requiredProps} | ||
/> | ||
); | ||
(component.find('EuiFocusTrap').prop('onDeactivation') as Function)(); | ||
|
||
expect(document.activeElement).toEqual(toggleButtonEl.current); | ||
}); | ||
|
||
it('refocuses the first nested toggle button on focus trap deactivation', () => { | ||
const toggleButtonEl = React.createRef<HTMLButtonElement>(); | ||
const toggleDiv = ( | ||
<div> | ||
<button ref={toggleButtonEl} tabIndex={-1} /> | ||
Comment on lines
+494
to
+498
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just want to note that this |
||
<button tabIndex={-1} /> | ||
</div> | ||
); | ||
|
||
const component = mount( | ||
<EuiPopover | ||
button={toggleDiv} | ||
isOpen={true} | ||
closePopover={() => {}} | ||
{...requiredProps} | ||
/> | ||
); | ||
(component.find('EuiFocusTrap').prop('onDeactivation') as Function)(); | ||
|
||
expect(document.activeElement).toEqual(toggleButtonEl.current); | ||
}); | ||
|
||
it('does not refocus if the toggle button is not focusable', () => { | ||
const toggleDivEl = React.createRef<HTMLDivElement>(); | ||
const toggleDiv = <div ref={toggleDivEl} />; | ||
|
||
const component = mount( | ||
<EuiPopover | ||
button={toggleDiv} | ||
isOpen={true} | ||
closePopover={() => {}} | ||
{...requiredProps} | ||
/> | ||
); | ||
(component.find('EuiFocusTrap').prop('onDeactivation') as Function)(); | ||
|
||
expect(document.activeElement).not.toEqual(toggleDivEl.current); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('getPopoverPositionFromAnchorPosition', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ import React, { | |
RefCallback, | ||
} from 'react'; | ||
import classNames from 'classnames'; | ||
import tabbable from 'tabbable'; | ||
import { tabbable, focusable } from 'tabbable'; | ||
|
||
import { CommonProps, NoArgCallback } from '../common'; | ||
import { FocusTarget, EuiFocusTrap, EuiFocusTrapProps } from '../focus_trap'; | ||
|
@@ -666,6 +666,17 @@ export class EuiPopover extends Component<Props, State> { | |
this.props.buttonRef && this.props.buttonRef(node); | ||
}; | ||
|
||
refocusButtonOnClose = () => { | ||
if (this.button) { | ||
const focusableItems = focusable(this.button); | ||
if (focusableItems.length) { | ||
const toggleButton = focusableItems[0]; | ||
requestAnimationFrame(() => toggleButton.focus(returnFocusConfig)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A couple notes here:
|
||
} | ||
} | ||
this.props.onTrapDeactivation?.(); | ||
}; | ||
|
||
render() { | ||
const { | ||
anchorClassName, | ||
|
@@ -776,7 +787,7 @@ export class EuiPopover extends Component<Props, State> { | |
{...focusTrapProps} | ||
returnFocus={returnFocus} // Ignore temporary state of indecisive focus | ||
initialFocus={initialFocus} | ||
onDeactivation={onTrapDeactivation} | ||
cee-chen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onDeactivation={this.refocusButtonOnClose} | ||
onClickOutside={this.onClickOutside} | ||
onEscapeKey={this.onEscapeKey} | ||
disabled={ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
**Bug fixes** | ||
|
||
- Fixed focus not being restored to popover toggles for certain complex popover children like `EuiContextMenu` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I upgraded tabbable because I wanted their new
focusable
API, and also generally just to get us on the latest. This has some amount of risk but I've briefly QA'd potentially affected components (EuiContextMenu, EuiDataGrid, EuiComboBox, EuiSuperSelect) and have not found any UX issues.