'use strict';
import DSComponent from '../../base/component/component';
import elementIdModifier from '../../base/tools/id-modifier/id-modifier';
/**
* Accordion component
*
* @class Accordion
* @extends DSComponent
* @property {HTMLElement} accordion - the accordion element
* @property {HTMLElement[]} items - the accordion items
* @property {HTMLButtonElement} openAllButton - the open all button
*/
class Accordion extends DSComponent {
private accordion: HTMLElement;
private items: HTMLElement[];
private openAllButton: HTMLButtonElement;
/**
* Creates an accordion component
*
* @param {HTMLElement} accordion - the accordion element
*/
constructor(accordion: HTMLElement) {
super(accordion);
this.accordion = accordion;
this.items = [].slice.call(accordion.querySelectorAll('.ds_accordion-item'));
this.openAllButton = accordion.querySelector('.js-open-all') as HTMLButtonElement;
}
/**
* Initialize the accordion
* - initialize each accordion item
* - initialize the open all button if present
*
* @returns {void}
*/
init(): void {
if (!this.isInitialised) {
this.items.forEach((item) => this.initAccordionItem(item));
if (this.openAllButton) {
this.initOpenAll();
}
this.isInitialised = true;
}
}
/**
* Initialize an accordion item
* - transform markup to button-driven version
* - attach event listener
* - set aria attributes
*
* @param {HTMLElement} item - the accordion item to initialize
* @returns {void}
*/
private initAccordionItem(item: HTMLElement): void {
// transform markup to button-driven version
const itemBody = item.querySelector('.ds_accordion-item__body') as HTMLElement;
const itemControl = item.querySelector('.ds_accordion-item__control') as HTMLInputElement;
const itemHeader = item.querySelector('.ds_accordion-item__header') as HTMLElement;
const itemIndicator = item.querySelector('.ds_accordion-item__indicator') as HTMLElement;
const itemLabelContent = item.querySelector('.ds_accordion-item__label span') as HTMLElement;
const itemTitle = itemHeader.querySelector('.ds_accordion-item__title') as HTMLHeadingElement;
// check for hash to open an accordion with
let accordionHasLocationHashInIt = false;
if (window.location.hash) {
try {
if (item.querySelector(window.location.hash)) {
accordionHasLocationHashInIt = true;
itemControl.checked = true;
}
} catch {
// hash is not a valid CSS selector or a selector for an item that is not found. ignore.
}
}
const startsOpen = itemControl.checked;
const itemButton = document.createElement('button');
itemTitle.classList.add('ds_accordion-item__title--button');
itemButton.classList.add('ds_accordion-item__button');
itemButton.classList.add('js-accordion-button');
itemButton.id = itemTitle.id + '-button';
itemButton.type = 'button';
// we keep the control present but make it unavailable in the tab order or to screen readers
// browsers will generally remember the state of the checkbox on back/forward navigation
itemControl.classList.remove('visually-hidden');
itemControl.classList.add('fully-hidden');
itemControl.setAttribute('tabindex', (-1).toString());
itemButton.innerHTML = itemTitle.innerHTML;
itemIndicator.setAttribute('aria-hidden', true.toString());
itemTitle.innerHTML = '';
itemTitle.insertBefore(itemButton, itemTitle.firstChild);
itemButton.appendChild(itemIndicator);
itemLabelContent.classList.add('fully-hidden');
const ID_MODIFIER = elementIdModifier();
item.id = item.id || `accordion-item-${ID_MODIFIER}`;
itemBody.id = itemBody.id || `accordion-item-${ID_MODIFIER}-body`;
if (startsOpen) {
item.classList.add('ds_accordion-item--open');
if (this.openAllButton) {
this.setOpenAllButton(this.checkAllOpen());
}
if (accordionHasLocationHashInIt) {
item.scrollIntoView();
}
}
itemButton.setAttribute('aria-expanded', startsOpen.toString());
itemButton.setAttribute('aria-controls', itemBody.id);
// events
itemButton.addEventListener('click', event => {
event.preventDefault();
this.toggleAccordionItem(item);
});
}
/**
* Initialize the open all button
* - attach event listener
* - set aria attributes
*
* @returns {void}
*/
private initOpenAll(): void {
this.openAllButton.addEventListener('click', () => {
function getAccordionItemForButton(button: HTMLButtonElement) {
return button.closest('.ds_accordion-item') as HTMLElement;
}
// if we're opening, open all unopened panels
// if we're closing, close all opened panels
const opening = !this.checkAllOpen();
const allPanelButtons: HTMLButtonElement[] = [].slice.call(this.accordion.querySelectorAll('.js-accordion-button'));
let panelsToToggle: HTMLButtonElement[];
if (opening) {
panelsToToggle = allPanelButtons.filter(button => !getAccordionItemForButton(button).classList.contains('ds_accordion-item--open'));
} else {
panelsToToggle = allPanelButtons.filter(button => getAccordionItemForButton(button).classList.contains('ds_accordion-item--open'));
}
panelsToToggle.forEach((button: HTMLButtonElement) => {
this.toggleAccordionItem(getAccordionItemForButton(button));
});
this.setOpenAllButton(opening);
});
this.openAllButton.setAttribute('aria-controls', this.items.map(item => item.id).join(' '));
this.openAllButton.setAttribute('aria-expanded', false.toString());
}
/**
* Toggle an accordion item
* - set aria attribute
* - set 'open' attribute
*
* @param {HTMLElement} item - the accordion item to toggle
* @returns {void}
*/
private toggleAccordionItem(item: HTMLElement): void {
const itemButton = item.querySelector('.js-accordion-button') as HTMLButtonElement;
const itemControl = item.querySelector('.ds_accordion-item__control') as HTMLInputElement;
const isOpen = item.classList.contains('ds_accordion-item--open');
if (!isOpen) {
item.classList.add('ds_accordion-item--open');
} else {
item.classList.remove('ds_accordion-item--open');
}
itemButton.setAttribute('aria-expanded', (!isOpen).toString());
itemControl.checked = !isOpen;
if (this.openAllButton) {
this.setOpenAllButton(this.checkAllOpen());
}
}
/**
* Set the open all button text and aria-expanded attribute
*
* @param {boolean} isOpen - true if all items are open, false otherwise
*/
private setOpenAllButton(isOpen: boolean) {
if (isOpen) {
this.openAllButton.innerHTML = 'Close all sections';
} else {
this.openAllButton.innerHTML = 'Open all sections';
}
this.openAllButton.setAttribute('aria-expanded', isOpen.toString())
}
/**
* Check if all accordion items are open
*
* @returns {boolean} - true if all items are open, false otherwise
*/
private checkAllOpen(): boolean {
const openItemsCount = this.accordion.querySelectorAll('.ds_accordion-item--open').length;
return (this.items.length === openItemsCount);
}
}
export default Accordion;