import { LitElement, css, html } from '@lion/core';
const uuid = () => Math.random().toString(36).substr(2, 10);
const setupContent = ({ element, uid, index }) => {
element.style.setProperty('order', index + 1);
element.setAttribute('id', `content-${uid}`);
element.setAttribute('aria-labelledby', `invoker-${uid}`);
};
const setupInvoker = ({ element, uid, index, clickHandler, keydownHandler }) => {
element.style.setProperty('order', index + 1);
element.firstElementChild.setAttribute('id', `invoker-${uid}`);
element.firstElementChild.setAttribute('aria-controls', `content-${uid}`);
element.firstElementChild.addEventListener('click', clickHandler);
element.firstElementChild.addEventListener('keydown', keydownHandler);
};
const cleanInvoker = (element, clickHandler, keydownHandler) => {
element.firstElementChild.removeAttribute('id');
element.firstElementChild.removeAttribute('aria-controls');
element.firstElementChild.removeEventListener('click', clickHandler);
element.firstElementChild.removeEventListener('keydown', keydownHandler);
};
const focusInvoker = element => {
element.firstElementChild.focus();
element.firstElementChild.setAttribute('focused', true);
};
const unfocusInvoker = element => {
element.firstElementChild.removeAttribute('focused');
};
const expandInvoker = element => {
element.setAttribute('expanded', true);
element.firstElementChild.setAttribute('expanded', true);
element.firstElementChild.setAttribute('aria-expanded', true);
};
const collapseInvoker = element => {
element.removeAttribute('expanded');
element.firstElementChild.removeAttribute('expanded');
element.firstElementChild.setAttribute('aria-expanded', false);
};
const expandContent = element => {
element.setAttribute('expanded', true);
};
const collapseContent = element => {
element.removeAttribute('expanded');
};
/**
* # webcomponent
*
* @customElement lion-accordion
* @extends LitElement
*/
export class LionAccordion extends LitElement {
static get properties() {
return {
/**
* index number of the focused accordion
*/
focusedIndex: {
type: Number,
},
/**
* array of indices of the expanded accordions
*/
expanded: {
type: Array,
},
};
}
static get styles() {
return [
css`
.accordion {
display: flex;
flex-direction: column;
}
.accordion ::slotted([slot='invoker']) {
margin: 0;
}
.accordion ::slotted([slot='invoker'][expanded]) {
font-weight: bold;
}
.accordion ::slotted([slot='content']) {
margin: 0;
visibility: hidden;
display: none;
}
.accordion ::slotted([slot='content'][expanded]) {
visibility: visible;
display: block;
}
`,
];
}
render() {
return html`
`;
}
constructor() {
super();
this.focusedIndex = null;
this.expanded = [];
this.styles = {};
}
firstUpdated() {
super.firstUpdated();
this.__setupSlots();
}
__setupSlots() {
const invokerSlot = this.shadowRoot.querySelector('slot[name=invoker]');
const handleSlotChange = () => {
this.__cleanStore();
this.__setupStore();
this.__updateFocused();
this.__updateExpanded();
};
invokerSlot.addEventListener('slotchange', handleSlotChange);
}
__setupStore() {
this.__store = [];
const invokers = this.querySelectorAll('[slot="invoker"]');
const contents = this.querySelectorAll('[slot="content"]');
if (invokers.length !== contents.length) {
// eslint-disable-next-line no-console
console.warn(
`The amount of invokers (${invokers.length}) doesn't match the amount of contents (${contents.length}).`,
);
}
invokers.forEach((invoker, index) => {
const uid = uuid();
const content = contents[index];
const entry = {
uid,
index,
invoker,
content,
clickHandler: this.__createInvokerClickHandler(index),
keydownHandler: this.__handleInvokerKeydown.bind(this),
};
setupContent({ element: entry.content, ...entry });
setupInvoker({ element: entry.invoker, ...entry });
unfocusInvoker(entry.invoker);
collapseContent(entry.content);
collapseInvoker(entry.invoker);
this.__store.push(entry);
});
}
__cleanStore() {
if (!this.__store) {
return;
}
this.__store.forEach(entry => {
cleanInvoker(entry.invoker, entry.clickHandler, entry.keydownHandler);
});
}
__createInvokerClickHandler(index) {
return () => {
this.focusedIndex = index;
this.__toggleExpanded(index);
};
}
__handleInvokerKeydown(e) {
switch (e.key) {
case 'ArrowDown':
case 'ArrowRight':
e.preventDefault();
if (this.focusedIndex + 2 <= this._pairCount) {
this.focusedIndex += 1;
}
break;
case 'ArrowUp':
case 'ArrowLeft':
e.preventDefault();
if (this.focusedIndex >= 1) {
this.focusedIndex -= 1;
}
break;
case 'Home':
e.preventDefault();
this.focusedIndex = 0;
break;
case 'End':
e.preventDefault();
this.focusedIndex = this._pairCount - 1;
break;
/* no default */
}
}
set focusedIndex(value) {
const stale = this.__focusedIndex;
this.__focusedIndex = value;
this.__updateFocused();
this.dispatchEvent(new Event('focused-changed'));
this.requestUpdate('focusedIndex', stale);
}
get focusedIndex() {
return this.__focusedIndex;
}
get _pairCount() {
return this.__store.length;
}
set expanded(value) {
const stale = this.__expanded;
this.__expanded = value;
this.__updateExpanded();
this.dispatchEvent(new Event('expanded-changed'));
this.requestUpdate('expanded', stale);
}
get expanded() {
return this.__expanded;
}
__updateFocused() {
if (!(this.__store && this.__store[this.focusedIndex])) {
return;
}
const previousInvoker = Array.from(this.children).find(
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
);
if (previousInvoker) {
unfocusInvoker(previousInvoker);
}
const { invoker: currentInvoker } = this.__store[this.focusedIndex];
if (currentInvoker) {
focusInvoker(currentInvoker);
}
}
__updateExpanded() {
if (!this.__store) {
return;
}
this.__store.forEach((entry, index) => {
const entryExpanded = this.expanded.indexOf(index) !== -1;
if (entryExpanded) {
expandInvoker(entry.invoker);
expandContent(entry.content);
} else {
collapseInvoker(entry.invoker);
collapseContent(entry.content);
}
});
}
__toggleExpanded(value) {
const { expanded } = this;
const index = expanded.indexOf(value);
if (index === -1) {
expanded.push(value);
} else {
expanded.splice(index, 1);
}
this.expanded = expanded;
}
}