Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 9 additions & 3 deletions assets/js/protocol/sidemenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
Expand Down
3 changes: 3 additions & 0 deletions components/sidebar-menu/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
115 changes: 115 additions & 0 deletions tests/unit/sidemenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import MzpSideMenu from '../../assets/js/protocol/sidemenu.js';

describe('sidemenu.js', function() {

beforeEach(function () {
const sidemenu = `
<nav class="mzp-c-sidemenu">
<section class="mzp-c-sidemenu-summary mzp-js-toggle" aria-controls="sidebar-menu">
<h3 class="mzp-c-sidemenu-label">Menu label</h3>
<ul>
<li>Menu title</li>
<li class="mzp-is-current">Menu item one</li>
</ul>
</section>

<section class="mzp-c-sidemenu-main" id="sidebar-menu">
<h4 class="mzp-c-sidemenu-heading"><a href="#">Menu title</a></h4>
<ul>
<li class="mzp-is-current"><a href="#">Menu item one</a></li>
<li><a href="#">Menu item two</a></li>
</ul>
</section>
</nav>
`;

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();
});
});
});