From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5
---
assets/js/concise/navigation.js | 225 ++++++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 185 insertions(+), 40 deletions(-)
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index d1fd398..3a9550f 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -2,11 +2,14 @@
constructor() {
this.counter = 0;
this.initElements();
- if (this.navs.length === 0) {
+ if (this.navs.size === 0) {
return;
}
this.openNav = null;
+ this.openSubmenu = null;
+ this.releaseFocusTrap = null;
+ this.clicked = new Set();
this.initListeners();
}
@@ -20,8 +23,8 @@
this.counter++;
}
if (nav.querySelector('.submenu')) {
- nav.addEventListener('mouseenter', this.hoverOnListener);
- nav.addEventListener('mouseleave', this.hoverOffListener);
+ nav.addEventListener('mouseenter', this.handleHoverOn.bind(this));
+ nav.addEventListener('mouseleave', this.handleHoverOff.bind(this));
}
let [
@@ -39,68 +42,167 @@
submenus: submenus,
submenuToggles: submenuToggles
}
- this.navs.set(navID, elements);
this.counter++;
+ submenus.forEach(menu => {
+ menu.id = 'submenu-'+this.counter;
+ menu.addEventListener('mouseenter', this.handleHoverOn.bind(this));
+ menu.addEventListener('mouseleave', this.handleHoverOff.bind(this));
+ this.counter++;
+ });
+ this.navs.set(navID, elements);
});
}
- navIDs() {
- return Array.from(this.navs.keys()).map((nav) => `#${nav}`);
- }
-
initListeners() {
this.clickListener = this.handleClick.bind(this);
- this.escapeListener = this.handleEscape.bind(this);
+ this.keysListener = this.handleKeys.bind(this);
this.hoverOnListener = this.handleHoverOn.bind(this);
this.hoverOffListener = this.handleHoverOff.bind(this);
document.addEventListener('click', this.clickListener);
}
handleClick(e) {
- if (this.openNav && !e.target.closest(this.openNav)) {
- this.toggleNav(false);
- }
- if (!e.target.closest(... this.navIDs())) {
+ if (this.navs.size === 0) {
return;
}
let toggle = e.target.closest('.toggle.main');
if (toggle) {
let nav = toggle.closest('nav');
- this.toggleNav(!nav.classList.contains('open'), nav.id);
+ let isOpening = !this.clicked.has(nav);
+ let shouldToggle = nav.classList.contains('open') !== isOpening;
+ if (shouldToggle) {
+ this.toggleNav(isOpening, nav.id);
+ }
+
+ if (isOpening) {
+ this.clicked.add(nav);
+ } else {
+ this.clicked.delete(nav);
+ }
+ return;
+ }
+
+ let submenuToggle = e.target.closest('[data-action="toggle-submenu"], .has-submenu .a')
+ if (submenuToggle) {
+ let li = submenuToggle.closest('li');
+ let isOpening = !this.clicked.has(li);
+ let shouldToggle = li.classList.contains('open') !== isOpening;
+
+ if (isOpening) {
+ this.clicked.add(li);
+ } else {
+ this.clicked.delete(li);
+ }
+ if (shouldToggle) {
+ this.toggleSubmenu(isOpening, li);
+ }
+ return;
+ }
+ if (!this.openNav) {
+ return;
+ }
+ let close = true;
+ for (let [navID, elements] of this.navs) {
+ if (e.target.closest('#'+navID)) {
+ close = false;
+ break;
+ }
+ }
+ if (close) {
+ this.toggleNav(false, this.openNav);
}
}
handleHoverOn(e) {
- console.log(e.target);
- let nav = e.target.closest('nav');
- if (nav) {
- this.toggleNav(true, nav.id);
+ let target = e.currentTarget;
+ if (this.clicked.has(target)) {
+ return;
}
- let submenu = e.target.closest('.has-submenu');
- if (submenu) {
- this.toggleSubmenu(true, submenu);
+ if (target.classList.contains('has-submenu')) {
+ this.toggleSubmenu(true, target);
+ } else if (target.tagName === 'NAV') {
+ if (!target.classList.contains('mobile')) {
+ this.toggleNav(true, target.id);
+ }
}
}
handleHoverOff(e) {
- console.log(e.target);
- let nav = e.target.closest('nav');
- if (nav) {
- this.toggleNav(false, nav.id);
+ let target = e.currentTarget;
+ if (this.clicked.has(target)) {
+ return;
}
- // let submenu = e.target.closest('.has-submenu');
- // if (submenu) {
- // this.toggleSubmenu(false, submenu);
- // }
+ if (target.classList.contains('has-submenu')) {
+ this.toggleSubmenu(false, target);
+ } else if (target.tagName === 'NAV') {
+ if (target.classList.contains('mobile')) {
+ return;
+ }
+ let nav = this.navs.get(target.id);
+ let shouldToggle = true;
+ for (let submenu of nav.submenus) {
+ if (this.clicked.has(submenu)) {
+ shouldToggle = false;
+ break;
+ }
+ }
+ if (shouldToggle) {
+ this.toggleNav(false, target.id);
+ }
+
+ }
}
- handleEscape(e) {
- if (this.openNav && e.key === 'Escape') {
- this.toggleNav(false, this.openNav);
+ handleKeys(e) {
+ if (!this.openNav) return;
+
+ switch (e.key) {
+ case 'Escape':
+ this.closeAll();
+ break;
+
+ case 'ArrowDown':
+ this.focusNextItem();
+ e.preventDefault();
+ break;
+
+ case 'ArrowUp':
+ this.focusPrevItem();
+ e.preventDefault();
+ break;
}
}
+ closeAll() {
+ let nav = this.navs.get(this.openNav);
+
+ if (nav && this.clicked.has(nav.nav)) {
+ this.clicked.delete(nav.nav);
+ }
+ this.toggleNav(false, this.openNav);
+ }
+
+ focusNextItem() {
+ const items = this.getFocusableItems();
+ const i = items.indexOf(document.activeElement);
+ const next = items[i + 1] || items[0];
+ next.focus();
+ }
+
+ focusPrevItem() {
+ const items = this.getFocusableItems();
+ const i = items.indexOf(document.activeElement);
+ const prev = items[i - 1] || items[items.length - 1];
+ prev.focus();
+ }
+
+ getFocusableItems() {
+ return Array.from(document.querySelectorAll(
+ 'nav.open a, nav.open button'
+ )).filter(el => !el.disabled && !el.closest('[hidden]') && !el.closest('[inert]'));
+ }
+
toggleNav(on, id) {
let nav = this.navs.get(id);
if (!nav) {
@@ -112,28 +214,62 @@
}
if (on) {
this.openNav = id;
- document.addEventListener('keydown', this.escapeListener);
+ document.addEventListener('keydown', this.keysListener);
+ if (nav.nav.classList.contains('mobile')) {
+ this.releaseFocusTrap = window.jvbA11y.trapFocus(nav.nav);
+ }
+
} else {
+ if (this.releaseFocusTrap) {
+ this.releaseFocusTrap();
+ this.releaseFocusTrap = null;
+ }
if (this.openNav === id) {
this.openNav = null;
}
- document.removeEventListener('keydown', this.escapeListener);
+ document.removeEventListener('keydown', this.keysListener);
+ if (!nav.nav.classList.contains('sidebar')) {
+ Array.from(nav.submenus).forEach(submenu => {
+ if(submenu.classList.contains('open')) {
+ this.toggleSubmenu(false, submenu);
+ }
+ });
+ }
Array.from(nav.submenus).forEach(submenu => {
- if(submenu.classList.contains('open')) {
- this.toggleSubmenu(false, submenu);
+ if (this.clicked.has(submenu)) {
+ this.clicked.delete(submenu);
}
});
}
- nav.nav.ariaExpanded = on;
nav.nav.classList.toggle('open', on);
- nav.ariaHidden = !on;
+
+ if (nav.nav.classList.contains('mobile')) {
+ const content = nav.nav.querySelector(':scope > ul');
+ if (content) content.inert = !on;
+ }
+
+ nav.nav.setAttribute('aria-expanded', on);
if (on) {
nav.nav.querySelector('a:not(.skip-to-content)')?.focus();
}
+
}
toggleSubmenu(on, submenu) {
+ if (on && this.openSubmenu && this.openSubmenu !== submenu) {
+ this.toggleSubmenu(false, this.openSubmenu);
+ }
+
+ if (on) {
+ this.openSubmenu = submenu;
+ } else if (this.openSubmenu === submenu) {
+ this.openSubmenu = null;
+ }
+
+
+
+
let [
toggle,
firstLink
@@ -142,16 +278,25 @@
submenu.querySelector('a')
];
+ if (!on) {
+ toggle.focus();
+ }
+ const content = submenu.querySelector(':scope > ul');
+ if (content) content.inert = !on;
+ let label = toggle.getAttribute('aria-label');
+ window.jvbA11y.announce(on ? `${label} expanded` : `${label} collapsed`);
+
submenu.classList.toggle('open', on);
- submenu.ariaHidden = !on;
- toggle.ariaExpanded = on;
+ toggle.setAttribute('aria-expanded', on);
if (on && firstLink) {
firstLink.focus();
}
}
+
}
document.addEventListener('DOMContentLoaded', function() {
+
window.jvbNav = new Navigation();
});
--
Gitblit v1.10.0