From 7dc4b1d4852e7f51bd16f405231f5c850b6e6c3a Mon Sep 17 00:00:00 2001 From: NUAAwqy <146524024+NUAAwqy@users.noreply.github.com> Date: Mon, 2 Nov 2026 11:45:46 +0000 Subject: [PATCH] Move aria-expanded to the side menu toggle (Fixes #999) `aria-expanded` was declared on `.mzp-c-sidemenu-main`, the content the toggle controls, so assistive tech was told the menu was collapsed even when the content was visible - including on desktop, where the toggle is hidden and the content is always expanded. Declare it on the `.mzp-js-toggle` element, which the template already links to the content via `aria-controls`, and derive its state from `mzp-is-active`. --- CHANGELOG.md | 1 + assets/js/protocol/sidemenu.js | 12 +++- components/sidebar-menu/readme.md | 3 + tests/unit/sidemenu.js | 115 ++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/unit/sidemenu.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e5146bbb..20061b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ Introducing theme variables! CSS variables beginning with `--theme-` will adjust * **css:** at2x mixin doesn't support single keyword values (#751) * **css:** Removed duplicate `mzp-a-slide-in-left` and `mzp-a-slide-in-right` `@keyframes` definitions +* **js:** Side Menu declared `aria-expanded` on `.mzp-c-sidemenu-main` instead of on the `.mzp-js-toggle` that controls it (#999) ## Migration Tips diff --git a/assets/js/protocol/sidemenu.js b/assets/js/protocol/sidemenu.js index c2ba37b7..4d99f45d 100644 --- a/assets/js/protocol/sidemenu.js +++ b/assets/js/protocol/sidemenu.js @@ -10,19 +10,25 @@ MzpSideMenu.init = () => { const menuToggle = document.querySelector('.mzp-js-toggle'); if (menu && menuMain && menuToggle) { + // `aria-expanded` belongs on the controlling element, which the template + // already links to the content with `aria-controls`, not on the content + // itself. Declaring it on `.mzp-c-sidemenu-main` announced visible content + // as collapsed, including on desktop where the toggle is hidden. + const setExpandedState = () => { + menuToggle.setAttribute('aria-expanded', menu.classList.contains('mzp-is-active')); + }; // Make the menu more accessible menuToggle.setAttribute('tabindex', '0'); menuToggle.setAttribute('role', 'button'); - menuMain.setAttribute('aria-expanded', 'false'); + setExpandedState(); // Toggle the sidebar menu menuToggle.addEventListener('click', (e) => { e.preventDefault(); menu.classList.toggle('mzp-is-active'); - const menuExpanded = menuMain.getAttribute('aria-expanded') === 'true' ? 'false' : 'true'; - menuMain.setAttribute('aria-expanded', menuExpanded); + setExpandedState(); }, false); } }; diff --git a/components/sidebar-menu/readme.md b/components/sidebar-menu/readme.md index 541db6e8..e3e184ef 100644 --- a/components/sidebar-menu/readme.md +++ b/components/sidebar-menu/readme.md @@ -36,3 +36,6 @@ MzpSideMenu.init(); - Make sure to initialize the component *after* the DOM has loaded. - Indicate the current page with the `mzp-is-current` class. +- Keep the `aria-controls` attribute on the `mzp-js-toggle` element pointing at the + `id` of `mzp-c-sidemenu-main`. `sidemenu.js` maintains `aria-expanded` on that + toggle, so assistive tech is told whether the menu content is shown. diff --git a/tests/unit/sidemenu.js b/tests/unit/sidemenu.js new file mode 100644 index 00000000..5fa79d5a --- /dev/null +++ b/tests/unit/sidemenu.js @@ -0,0 +1,115 @@ +import MzpSideMenu from '../../assets/js/protocol/sidemenu.js'; + +describe('sidemenu.js', function() { + + beforeEach(function () { + const sidemenu = ` + + `; + + document.documentElement.insertAdjacentHTML('beforeend', sidemenu); + }); + + afterEach(function(){ + const node = document.querySelector('.mzp-c-sidemenu'); + if (node && node.parentNode) { + node.parentNode.removeChild(node); + } + }); + + describe('aria states', function () { + + it('should declare aria-expanded on the toggle, not on the menu content', function() { + const toggle = document.querySelector('.mzp-js-toggle'); + const menu = document.querySelector('.mzp-c-sidemenu-main'); + + MzpSideMenu.init(); + + expect(menu.hasAttribute('aria-expanded')).toBeFalse(); + expect(toggle.getAttribute('aria-expanded')).toBe('false'); + }); + + it('should keep aria-controls pointing at the menu content', function() { + const toggle = document.querySelector('.mzp-js-toggle'); + const menu = document.querySelector('.mzp-c-sidemenu-main'); + + MzpSideMenu.init(); + + expect(toggle.getAttribute('aria-controls')).toBe(menu.id); + }); + + it('should update aria-expanded on the toggle when the menu is opened and closed', function() { + const toggle = document.querySelector('.mzp-js-toggle'); + + MzpSideMenu.init(); + + toggle.click(); + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + + toggle.click(); + expect(toggle.getAttribute('aria-expanded')).toBe('false'); + }); + + it('should not report an already expanded menu as collapsed', function() { + const nav = document.querySelector('.mzp-c-sidemenu'); + const toggle = document.querySelector('.mzp-js-toggle'); + const menu = document.querySelector('.mzp-c-sidemenu-main'); + + nav.classList.add('mzp-is-active'); + + MzpSideMenu.init(); + + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + expect(menu.hasAttribute('aria-expanded')).toBeFalse(); + }); + }); + + describe('interactions', function () { + + it('should open and close as expected', function() { + const nav = document.querySelector('.mzp-c-sidemenu'); + const toggle = document.querySelector('.mzp-js-toggle'); + + MzpSideMenu.init(); + + toggle.click(); + expect(nav.classList.contains('mzp-is-active')).toBeTrue(); + + toggle.click(); + expect(nav.classList.contains('mzp-is-active')).toBeFalse(); + }); + + it('should keep the toggle focusable and exposed as a button', function() { + const toggle = document.querySelector('.mzp-js-toggle'); + + MzpSideMenu.init(); + + expect(toggle.getAttribute('role')).toBe('button'); + expect(toggle.getAttribute('tabindex')).toBe('0'); + }); + + it('should do nothing when there is no side menu', function() { + document.querySelector('.mzp-c-sidemenu').remove(); + + expect(() => { + MzpSideMenu.init(); + }).not.toThrow(); + }); + }); +});