diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index d66f49fca0c..faca98fbfbd 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -6,6 +6,7 @@ import PanEvents, { import { cancel, later, schedule } from "@ember/runloop"; import Docking from "discourse/mixins/docking"; import MountWidget from "discourse/components/mount-widget"; +import Mousetrap from "mousetrap"; import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change"; import { observes } from "discourse-common/utils/decorators"; import { topicTitleDecorators } from "discourse/components/topic-title"; @@ -25,6 +26,7 @@ const SiteHeaderComponent = MountWidget.extend( _scheduledMovingAnimation: null, _scheduledRemoveAnimate: null, _topic: null, + _mousetrap: null, @observes( "currentUser.unread_notifications", @@ -209,6 +211,7 @@ const SiteHeaderComponent = MountWidget.extend( this.dispatch("notifications:changed", "user-notifications"); this.dispatch("header:keyboard-trigger", "header"); this.dispatch("search-autocomplete:after-complete", "search-term"); + this.dispatch("user-menu:navigation", "user-menu"); this.appEvents.on("dom:clean", this, "_cleanDom"); @@ -236,6 +239,26 @@ const SiteHeaderComponent = MountWidget.extend( once: true, }); } + + const header = document.querySelector("header.d-header"); + const mousetrap = new Mousetrap(header); + mousetrap.bind(["right", "left"], (e) => { + const activeTab = document.querySelector(".glyphs .menu-link.active"); + + if (activeTab) { + let focusedTab = document.activeElement; + if (!focusedTab.dataset.tabNumber) { + focusedTab = activeTab; + } + + this.appEvents.trigger("user-menu:navigation", { + key: e.key, + tabNumber: Number(focusedTab.dataset.tabNumber), + }); + } + }); + + this.set("_mousetrap", mousetrap); }, _cleanDom() { @@ -257,6 +280,8 @@ const SiteHeaderComponent = MountWidget.extend( cancel(this._scheduledRemoveAnimate); window.cancelAnimationFrame(this._scheduledMovingAnimation); + this._mousetrap.unbind(["right", "left"]); + document.removeEventListener("click", this._dismissFirstNotification); }, diff --git a/app/assets/javascripts/discourse/app/widgets/button.js b/app/assets/javascripts/discourse/app/widgets/button.js index 01df29e6cf7..6b08400e459 100644 --- a/app/assets/javascripts/discourse/app/widgets/button.js +++ b/app/assets/javascripts/discourse/app/widgets/button.js @@ -51,6 +51,7 @@ export const ButtonClass = { attributes["aria-selected"] = tab["aria-selected"]; attributes["tabindex"] = tab["tabindex"]; attributes["aria-controls"] = tab["aria-controls"]; + attributes["id"] = attrs.id; } if (attrs.disabled) { diff --git a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js index c13bd2705b5..8e2c0c83303 100644 --- a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js +++ b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js @@ -40,9 +40,13 @@ export default createWidget("quick-access-panel", { return Promise.resolve([]); }, + buildId() { + return this.key; + }, + buildAttributes() { const attributes = this.attrs; - attributes["aria-labelledby"] = this.key; + attributes["aria-labelledby"] = attributes.currentQuickAccess; attributes["tabindex"] = "0"; attributes["role"] = "tabpanel"; diff --git a/app/assets/javascripts/discourse/app/widgets/user-menu.js b/app/assets/javascripts/discourse/app/widgets/user-menu.js index 5ad3e5309fa..09cda2bb125 100644 --- a/app/assets/javascripts/discourse/app/widgets/user-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/user-menu.js @@ -1,6 +1,6 @@ +import { later } from "@ember/runloop"; import { createWidget } from "discourse/widgets/widget"; import { h } from "virtual-dom"; -import { later } from "@ember/runloop"; const UserMenuAction = { QUICK_ACCESS: "quickAccess", @@ -50,6 +50,12 @@ createWidget("user-menu-links", { glyph.href = null; } + if (glyph.className) { + glyph.className += " menu-link"; + } else { + glyph.className = "menu-link"; + } + glyph.role = "tab"; glyph.tabAttrs = this._tabAttrs(glyph.actionParam); @@ -59,7 +65,8 @@ createWidget("user-menu-links", { profileGlyph() { return { title: Titles["profile"], - className: "user-preferences-link", + className: "user-preferences-link menu-link", + id: QuickAccess.PROFILE, icon: "user", action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.PROFILE, @@ -72,7 +79,8 @@ createWidget("user-menu-links", { notificationsGlyph() { return { title: Titles["notifications"], - className: "user-notifications-link", + className: "user-notifications-link menu-link", + id: QuickAccess.NOTIFICATIONS, icon: "bell", action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.NOTIFICATIONS, @@ -87,7 +95,8 @@ createWidget("user-menu-links", { title: Titles["bookmarks"], action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.BOOKMARKS, - className: "user-bookmarks-link", + className: "user-bookmarks-link menu-link", + id: QuickAccess.BOOKMARKS, icon: "bookmark", data: { url: `${this.attrs.path}/activity/bookmarks` }, "aria-label": "user.bookmarks", @@ -101,7 +110,8 @@ createWidget("user-menu-links", { title: Titles["messages"], action: UserMenuAction.QUICK_ACCESS, actionParam: QuickAccess.MESSAGES, - className: "user-pms-link", + className: "user-pms-link menu-link", + id: QuickAccess.MESSAGES, icon: "envelope", data: { url: `${this.attrs.path}/messages` }, role: "tab", @@ -116,15 +126,17 @@ createWidget("user-menu-links", { return this.attach("link", link); }, - glyphHtml(glyph) { + glyphHtml(glyph, idx) { if (this.isActive(glyph)) { glyph = this.markAsActive(glyph); } + glyph.data["tab-number"] = `${idx}`; + return this.attach("flat-button", glyph); }, html() { - const glyphs = []; + const glyphs = [this.notificationsGlyph()]; if (extraGlyphs) { extraGlyphs.forEach((g) => { @@ -140,7 +152,6 @@ createWidget("user-menu-links", { }); } - glyphs.push(this.notificationsGlyph()); glyphs.push(this.bookmarksGlyph()); if (this.siteSettings.enable_personal_messages || this.currentUser.staff) { @@ -153,7 +164,7 @@ createWidget("user-menu-links", { h( "div.glyphs", { attributes: { "aria-label": "Menu links", role: "tablist" } }, - glyphs.map((l) => this.glyphHtml(l)) + glyphs.map((l, index) => this.glyphHtml(l, index)) ), ]); }, @@ -194,6 +205,25 @@ export default createWidget("user-menu", { showLogoutButton: true, }, + userMenuNavigation(nav) { + const maxTabNumber = document.querySelectorAll(".glyphs button").length - 1; + const isLeft = nav.key === "ArrowLeft"; + + let nextTab = isLeft ? nav.tabNumber - 1 : nav.tabNumber + 1; + + if (isLeft && nextTab < 0) { + nextTab = maxTabNumber; + } + + if (!isLeft && nextTab > maxTabNumber) { + nextTab = 0; + } + + document + .querySelector(`.menu-link[role='tab'][data-tab-number='${nextTab}']`) + .focus(); + }, + defaultState() { return { currentQuickAccess: QuickAccess.NOTIFICATIONS, @@ -212,7 +242,7 @@ export default createWidget("user-menu", { path, currentQuickAccess, }), - this.quickAccessPanel(path, titleKey), + this.quickAccessPanel(path, titleKey, currentQuickAccess), ]; return result; @@ -269,13 +299,14 @@ export default createWidget("user-menu", { } }, - quickAccessPanel(path, titleKey) { + quickAccessPanel(path, titleKey, currentQuickAccess) { const { showLogoutButton } = this.settings; // This deliberately does NOT fallback to a default quick access panel. return this.attach(`quick-access-${this.state.currentQuickAccess}`, { path, showLogoutButton, titleKey, + currentQuickAccess, }); }, }); diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 3e808b29467..936a6cced5d 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -389,11 +389,6 @@ } } -.user-notifications-link { - // keep this in leftmost position consistently - order: -1; -} - div.menu-links-header { width: 100%; .menu-links-row { @@ -416,6 +411,10 @@ div.menu-links-header { flex: 1 1 auto; padding: 0.65em 0.25em 0.75em; justify-content: center; + + svg { + pointer-events: none; + } } }