blob: 4cdb6c0af95b4d3d54dfd93d27e327cb0d5e6613 [file] [log] [blame]
/**
* @license
* Copyright 2020 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/**
* Controller for an accordion element. This class will add event
* listeners to close and open the panels of an accordion. When
* initialized it will select the active panel based on the URL
* hash for the requested page.
*
* Accordions must have the following structure:
*
* <div class="js-accordion">
* <a role="button" class="js-accordionTrigger" href="#panel-1" aria-expanded="false" aria-controls="first-panel" id="first-accordion">
* Title
* </a>
* <div class="js-accordionPanel" id="first-panel" role="region" aria-labelledby="first-accordion" aria-hidden="true">
* Panel Content
* </div>
* <a role="button" class="js-accordionTrigger" href="#panel-2" aria-expanded="false" aria-controls="second-panel" id="second-accordion">
* Title
* </a>
* <div class="js-accordionPanel" id="second-panel" role="region" aria-labelledby="second-accordion" aria-hidden="true">
* Panel Content
* </div>
* </div>
*/
export class AccordionController {
constructor(accordion) {
this.accordion = accordion;
this.triggers = [...this.accordion.querySelectorAll('.js-accordionTrigger')];
this.activeTrigger = this.triggers[0];
this.init();
}
init() {
this.accordion.addEventListener('click', e => {
if (e.target.classList.contains('js-accordionTrigger')) {
this.select(e.target);
}
});
this.accordion.addEventListener('keydown', e => {
if (e.target.classList.contains('js-accordionTrigger')) {
this.handleKeyPress(e);
}
});
const activeHash = document.querySelector(`a[href=${JSON.stringify(window.location.hash)}]`);
const initialTrigger =
this.triggers.find(
trigger => this.getPanel(trigger).contains(activeHash) || trigger.contains(activeHash)
) || this.activeTrigger;
this.select(initialTrigger);
}
getPanel(trigger) {
return document.getElementById(trigger.getAttribute('aria-controls'));
}
select(target) {
const isExpanded = target.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
target.setAttribute('aria-expanded', 'true');
this.getPanel(target).setAttribute('aria-hidden', 'false');
}
if (this.activeTrigger !== target) {
this.activeTrigger.setAttribute('aria-expanded', 'false');
this.getPanel(this.activeTrigger).setAttribute('aria-hidden', 'true');
}
this.activeTrigger = target;
}
handleKeyPress(e) {
const target = e.target;
const key = e.which;
const SPACE = 32;
const PAGE_UP = 33;
const PAGE_DOWN = 34;
const END = 35;
const HOME = 36;
const ARROW_UP = 38;
const ARROW_DOWN = 40;
switch (key) {
case PAGE_UP:
case PAGE_DOWN:
case ARROW_UP:
case ARROW_DOWN:
const index = this.triggers.indexOf(target);
const direction = [PAGE_UP, ARROW_UP].includes(key) ? -1 : 1;
const newIndex = (index + this.triggers.length + direction) % this.triggers.length;
this.triggers[newIndex].focus();
e.preventDefault();
break;
case END:
this.triggers[this.triggers.length - 1].focus();
e.preventDefault();
break;
case HOME:
this.triggers[0].focus();
e.preventDefault();
break;
case SPACE:
this.select(target);
default:
break;
}
}
}