A11Y: Close header dropdown menus on focusout (#27901)

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
Kris
2024-07-16 09:11:26 -04:00
committed by GitHub
parent c74fa300e7
commit 0d4492c7b7
7 changed files with 110 additions and 6 deletions

View File

@@ -4,7 +4,7 @@ import { getOwner } from "@ember/application";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { modifier } from "ember-modifier";
import { modifier as modifierFn } from "ember-modifier";
import { and, eq, not, or } from "truth-helpers";
import PluginOutlet from "discourse/components/plugin-outlet";
import DAG from "discourse/lib/dag";
@@ -19,6 +19,9 @@ import SearchMenuWrapper from "./header/search-menu-wrapper";
import UserMenuWrapper from "./header/user-menu-wrapper";
const SEARCH_BUTTON_ID = "search-button";
const USER_BUTTON_ID = "toggle-current-user";
const HAMBURGER_BUTTON_ID = "toggle-hamburger-menu";
const PANEL_SELECTOR = ".panel-body";
let headerButtons;
resetHeaderButtons();
@@ -47,12 +50,13 @@ export default class GlimmerHeader extends Component {
@tracked skipSearchContext = this.site.mobileView;
appEventsListeners = modifier(() => {
appEventsListeners = modifierFn(() => {
this.appEvents.on(
"header:keyboard-trigger",
this,
this.headerKeyboardTrigger
);
return () => {
this.appEvents.off(
"header:keyboard-trigger",
@@ -62,6 +66,60 @@ export default class GlimmerHeader extends Component {
};
});
handleFocus = modifierFn((element) => {
const panelBody = element.querySelector(PANEL_SELECTOR);
if (!panelBody) {
return;
}
let isKeyboardEvent = false;
const handleKeydown = (event) => {
if (event.key) {
isKeyboardEvent = true;
}
};
// avoid triggering focusout on click
// otherwise we can double-trigger the menu toggle
const handleMousedown = () => {
isKeyboardEvent = false;
};
const focusOutHandler = (event) => {
if (!isKeyboardEvent) {
return;
}
if (!panelBody.contains(event.relatedTarget)) {
this.closeCurrentMenu();
}
};
panelBody.addEventListener("keydown", handleKeydown);
panelBody.addEventListener("mousedown", handleMousedown);
panelBody.addEventListener("focusout", focusOutHandler);
return () => {
panelBody.removeEventListener("keydown", handleKeydown);
panelBody.removeEventListener("mousedown", handleMousedown);
panelBody.removeEventListener("focusout", focusOutHandler);
};
});
@action
closeCurrentMenu() {
if (this.search.visible) {
this.toggleSearchMenu();
} else if (this.header.userVisible) {
this.toggleUserMenu();
document.getElementById(USER_BUTTON_ID)?.focus();
} else if (this.header.hamburgerVisible) {
this.toggleHamburger();
document.getElementById(HAMBURGER_BUTTON_ID)?.focus();
}
}
@action
headerKeyboardTrigger(msg) {
switch (msg.type) {
@@ -220,14 +278,21 @@ export default class GlimmerHeader extends Component {
{{/if}}
{{#if this.search.visible}}
<SearchMenuWrapper @closeSearchMenu={{this.toggleSearchMenu}} />
<SearchMenuWrapper
@closeSearchMenu={{this.toggleSearchMenu}}
{{this.handleFocus}}
/>
{{else if this.header.hamburgerVisible}}
<HamburgerDropdownWrapper
@toggleNavigationMenu={{this.toggleNavigationMenu}}
@sidebarEnabled={{@sidebarEnabled}}
{{this.handleFocus}}
/>
{{else if this.header.userVisible}}
<UserMenuWrapper @toggleUserMenu={{this.toggleUserMenu}} />
<UserMenuWrapper
@toggleUserMenu={{this.toggleUserMenu}}
{{this.handleFocus}}
/>
{{/if}}
{{#if

View File

@@ -97,6 +97,7 @@ export default class HamburgerDropdownWrapper extends Component {
secondaryTargetSelector=".hamburger-dropdown"
)
}}
...attributes
>
<SidebarHamburgerDropdown
@forceMainSidebarPanel={{this.forceMainSidebarPanel}}

View File

@@ -1,7 +1,7 @@
import SearchMenuPanel from "../search-menu-panel";
const SearchMenuWrapper = <template>
<div class="search-menu glimmer-search-menu" aria-live="polite">
<div class="search-menu glimmer-search-menu" aria-live="polite" ...attributes>
<SearchMenuPanel @closeSearchMenu={{@closeSearchMenu}} />
</div>
</template>;

View File

@@ -36,6 +36,7 @@ export default class UserDropdown extends Component {
>
<PluginOutlet @name="user-dropdown-button__before" />
<button
id="toggle-current-user"
class="icon btn-flat"
aria-haspopup="true"
aria-expanded={{@active}}

View File

@@ -51,6 +51,7 @@ export default class UserMenuWrapper extends Component {
secondaryTargetSelector=".user-menu-panel"
)
}}
...attributes
>
<UserMenu @closeUserMenu={{@toggleUserMenu}} />
</div>

View File

@@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { schedule } from "@ember/runloop";
import { service } from "@ember/service";
import { or } from "truth-helpers";
import DeferredRender from "discourse/components/deferred-render";
@@ -20,6 +21,16 @@ export default class SidebarHamburgerDropdown extends Component {
this.appEvents.trigger("sidebar-hamburger-dropdown:rendered");
}
@action
focusFirstLink() {
schedule("afterRender", () => {
const firstLink = document.querySelector(".sidebar-hamburger-dropdown a");
if (firstLink) {
firstLink.focus();
}
});
}
get collapsableSections() {
if (
this.siteSettings.navigation_menu === "header dropdown" &&
@@ -41,7 +52,10 @@ export default class SidebarHamburgerDropdown extends Component {
<div class="panel-body">
<div class="panel-body-contents">
<DeferredRender>
<div class="sidebar-hamburger-dropdown">
<div
class="sidebar-hamburger-dropdown"
{{didInsert this.focusFirstLink}}
>
{{#if
(or this.sidebarState.showMainPanel @forceMainSidebarPanel)
}}