Skip to content

Commit 5d68193

Browse files
authoredAug 20, 2024··
actionmenu: ensure focus falls back to invoking element when dialogs are closed (#2983)
1 parent 8f06f7a commit 5d68193

File tree

3 files changed

+42
-7
lines changed

3 files changed

+42
-7
lines changed
 

‎.changeset/brave-bugs-tap.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/view-components': patch
3+
---
4+
5+
Ensure ActionMenu restores focus on close of a dialog, if a menu item opened that dialog

‎app/components/primer/alpha/action_menu/action_menu_element.ts

+20-7
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class ActionMenuElement extends HTMLElement {
207207
if (dialogInvoker) {
208208
const dialog = this.ownerDocument.getElementById(dialogInvoker.getAttribute('data-show-dialog-id') || '')
209209

210-
if (dialog && this.contains(dialogInvoker) && this.contains(dialog)) {
210+
if (dialog && this.contains(dialogInvoker)) {
211211
this.#handleDialogItemActivated(event, dialog)
212212
return
213213
}
@@ -243,20 +243,33 @@ export class ActionMenuElement extends HTMLElement {
243243
}
244244

245245
#handleDialogItemActivated(event: Event, dialog: HTMLElement) {
246-
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = 'none'
246+
if (this.contains(dialog)) {
247+
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = 'none'
248+
}
247249
const dialog_controller = new AbortController()
248250
const {signal} = dialog_controller
249251
const handleDialogClose = () => {
250252
dialog_controller.abort()
251-
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = ''
252-
if (this.#isOpen()) {
253-
this.#hide()
253+
if (this.contains(dialog)) {
254+
this.querySelector<HTMLElement>('.ActionListWrap')!.style.display = ''
255+
if (this.#isOpen()) {
256+
this.#hide()
257+
}
254258
}
255259
const activeElement = this.ownerDocument.activeElement
256260
const lostFocus = this.ownerDocument.activeElement === this.ownerDocument.body
257261
const focusInClosedMenu = this.contains(activeElement)
258-
if (lostFocus || focusInClosedMenu) {
259-
setTimeout(() => this.invokerElement?.focus(), 0)
262+
const focusInDialog = dialog.contains(activeElement)
263+
if (lostFocus || focusInClosedMenu || focusInDialog) {
264+
setTimeout(() => {
265+
// if the activeElement has changed after a task, then it's likely
266+
// that other JS has tried to shift focus. We should respect that
267+
// focus shift as long as it's not back at the document.
268+
const newActiveElement = this.ownerDocument.activeElement
269+
if (newActiveElement === activeElement || newActiveElement === this.ownerDocument.body) {
270+
this.invokerElement?.focus()
271+
}
272+
}, 0)
260273
}
261274
}
262275
// a modal <dialog> element will close all popovers

‎test/system/alpha/action_menu_test.rb

+17
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def click_on_fourth_item
5656
click_on_item(4)
5757
end
5858

59+
def close_dialog
60+
find("[data-close-dialog-id][aria-label=Close]").click
61+
end
62+
5963
def focus_on_invoker_button
6064
page.evaluate_script(<<~JS)
6165
document.querySelector('action-menu button[aria-controls]').focus()
@@ -340,6 +344,19 @@ def test_opens_dialog
340344
refute_selector "action-menu ul li"
341345
end
342346

347+
def test_open_then_closing_dialog_restores_focus
348+
visit_preview(:opens_dialog)
349+
350+
click_on_invoker_button
351+
click_on_second_item
352+
353+
assert_selector "dialog[open]"
354+
355+
close_dialog
356+
357+
assert_equal page.evaluate_script("document.activeElement").text, "Menu"
358+
end
359+
343360
def test_opens_dialog_on_keydown
344361
visit_preview(:opens_dialog)
345362

0 commit comments

Comments
 (0)
Please sign in to comment.