Merge pull request #1488 from ing-bank/refactor/accordion
feat(accordion): refactor for readability & extensibility
This commit is contained in:
commit
fdc62b4f7a
2 changed files with 166 additions and 168 deletions
5
.changeset/silent-seahorses-battle.md
Normal file
5
.changeset/silent-seahorses-battle.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/accordion': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Make methods for handling invoker/content protected parts of the Accordion base class, so that they can be overridden easily. Refactor for readability.
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
import { LitElement, css, html } from '@lion/core';
|
import { LitElement, css, html } from '@lion/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,111 +13,6 @@ import { LitElement, css, html } from '@lion/core';
|
||||||
|
|
||||||
const uuid = () => Math.random().toString(36).substr(2, 10);
|
const uuid = () => Math.random().toString(36).substr(2, 10);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} opts
|
|
||||||
* @param {HTMLElement} opts.element
|
|
||||||
* @param {string} opts.uid
|
|
||||||
* @param {number} opts.index
|
|
||||||
*/
|
|
||||||
const setupContent = ({ element, uid, index }) => {
|
|
||||||
element.style.setProperty('order', `${index + 1}`);
|
|
||||||
element.setAttribute('id', `content-${uid}`);
|
|
||||||
element.setAttribute('aria-labelledby', `invoker-${uid}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} opts
|
|
||||||
* @param {HTMLElement} opts.element
|
|
||||||
* @param {string} opts.uid
|
|
||||||
* @param {number} opts.index
|
|
||||||
* @param {EventHandlerNonNull} opts.clickHandler
|
|
||||||
* @param {EventHandlerNonNull} opts.keydownHandler
|
|
||||||
*/
|
|
||||||
const setupInvoker = ({ element, uid, index, clickHandler, keydownHandler }) => {
|
|
||||||
element.style.setProperty('order', `${index + 1}`);
|
|
||||||
const firstChild = element.firstElementChild;
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.setAttribute('id', `invoker-${uid}`);
|
|
||||||
firstChild.setAttribute('aria-controls', `content-${uid}`);
|
|
||||||
firstChild.addEventListener('click', clickHandler);
|
|
||||||
firstChild.addEventListener('keydown', keydownHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @param {EventHandlerNonNull} clickHandler
|
|
||||||
* @param {EventHandlerNonNull} keydownHandler
|
|
||||||
*/
|
|
||||||
const cleanInvoker = (element, clickHandler, keydownHandler) => {
|
|
||||||
const firstChild = element.firstElementChild;
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.removeAttribute('id');
|
|
||||||
firstChild.removeAttribute('aria-controls');
|
|
||||||
firstChild.removeEventListener('click', clickHandler);
|
|
||||||
firstChild.removeEventListener('keydown', keydownHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
const focusInvoker = element => {
|
|
||||||
const firstChild = /** @type {HTMLElement|null} */ (element.firstElementChild);
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.focus();
|
|
||||||
firstChild.setAttribute('focused', `${true}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
const unfocusInvoker = element => {
|
|
||||||
const firstChild = element.firstElementChild;
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.removeAttribute('focused');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
const expandInvoker = element => {
|
|
||||||
element.setAttribute('expanded', `${true}`);
|
|
||||||
const firstChild = element.firstElementChild;
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.setAttribute('expanded', `${true}`);
|
|
||||||
firstChild.setAttribute('aria-expanded', `${true}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
const collapseInvoker = element => {
|
|
||||||
element.removeAttribute('expanded');
|
|
||||||
const firstChild = element.firstElementChild;
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.removeAttribute('expanded');
|
|
||||||
firstChild.setAttribute('aria-expanded', `${false}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
const expandContent = element => {
|
|
||||||
element.setAttribute('expanded', `${true}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
const collapseContent = element => {
|
|
||||||
element.removeAttribute('expanded');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # <lion-accordion> webcomponent
|
* # <lion-accordion> webcomponent
|
||||||
*
|
*
|
||||||
|
|
@ -171,13 +67,34 @@ export class LionAccordion extends LitElement {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
/**
|
||||||
return html`
|
* @param {number} value
|
||||||
<div class="accordion">
|
*/
|
||||||
<slot name="invoker"></slot>
|
set focusedIndex(value) {
|
||||||
<slot name="content"></slot>
|
const stale = this.__focusedIndex;
|
||||||
</div>
|
this.__focusedIndex = value;
|
||||||
`;
|
this.__updateFocused();
|
||||||
|
this.dispatchEvent(new Event('focused-changed'));
|
||||||
|
this.requestUpdate('focusedIndex', stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
get focusedIndex() {
|
||||||
|
return this.__focusedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number[]} value
|
||||||
|
*/
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -196,7 +113,10 @@ export class LionAccordion extends LitElement {
|
||||||
*/
|
*/
|
||||||
this.__focusedIndex = -1;
|
this.__focusedIndex = -1;
|
||||||
|
|
||||||
/** @type {number[]} */
|
/**
|
||||||
|
* @type {number[]}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
this.__expanded = [];
|
this.__expanded = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,6 +126,15 @@ export class LionAccordion extends LitElement {
|
||||||
this.__setupSlots();
|
this.__setupSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="accordion">
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -251,30 +180,15 @@ export class LionAccordion extends LitElement {
|
||||||
clickHandler: this.__createInvokerClickHandler(index),
|
clickHandler: this.__createInvokerClickHandler(index),
|
||||||
keydownHandler: this.__handleInvokerKeydown.bind(this),
|
keydownHandler: this.__handleInvokerKeydown.bind(this),
|
||||||
};
|
};
|
||||||
setupContent({ element: entry.content, ...entry });
|
this._setupContent(entry);
|
||||||
setupInvoker({ element: entry.invoker, ...entry });
|
this._setupInvoker(entry);
|
||||||
unfocusInvoker(entry.invoker);
|
this._unfocusInvoker(entry);
|
||||||
collapseContent(entry.content);
|
this._collapse(entry);
|
||||||
collapseInvoker(entry.invoker);
|
|
||||||
this.__store.push(entry);
|
this.__store.push(entry);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__cleanStore() {
|
|
||||||
if (!this.__store) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.__store.forEach(entry => {
|
|
||||||
cleanInvoker(entry.invoker, entry.clickHandler, entry.keydownHandler);
|
|
||||||
});
|
|
||||||
this.__store = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -318,18 +232,6 @@ export class LionAccordion extends LitElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
@ -337,36 +239,116 @@ export class LionAccordion extends LitElement {
|
||||||
return this.__store.length;
|
return this.__store.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
set expanded(value) {
|
/**
|
||||||
const stale = this.__expanded;
|
* @param {StoreEntry} entry
|
||||||
this.__expanded = value;
|
* @protected
|
||||||
this.__updateExpanded();
|
*/
|
||||||
this.dispatchEvent(new Event('expanded-changed'));
|
_setupContent(entry) {
|
||||||
this.requestUpdate('expanded', stale);
|
const { content, index, uid } = entry;
|
||||||
|
content.style.setProperty('order', `${index + 1}`);
|
||||||
|
content.setAttribute('id', `content-${uid}`);
|
||||||
|
content.setAttribute('aria-labelledby', `invoker-${uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get expanded() {
|
/**
|
||||||
return this.__expanded;
|
* @param {StoreEntry} entry
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_setupInvoker(entry) {
|
||||||
|
const { invoker, uid, index, clickHandler, keydownHandler } = entry;
|
||||||
|
invoker.style.setProperty('order', `${index + 1}`);
|
||||||
|
const firstChild = invoker.firstElementChild;
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.setAttribute('id', `invoker-${uid}`);
|
||||||
|
firstChild.setAttribute('aria-controls', `content-${uid}`);
|
||||||
|
firstChild.addEventListener('click', clickHandler);
|
||||||
|
firstChild.addEventListener('keydown', keydownHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StoreEntry} entry
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_cleanInvoker(entry) {
|
||||||
|
const { invoker, clickHandler, keydownHandler } = entry;
|
||||||
|
const firstChild = invoker.firstElementChild;
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.removeAttribute('id');
|
||||||
|
firstChild.removeAttribute('aria-controls');
|
||||||
|
firstChild.removeEventListener('click', clickHandler);
|
||||||
|
firstChild.removeEventListener('keydown', keydownHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StoreEntry} entry
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_focusInvoker(entry) {
|
||||||
|
const { invoker } = entry;
|
||||||
|
const firstChild = /** @type {HTMLElement|null} */ (invoker.firstElementChild);
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.focus();
|
||||||
|
firstChild.setAttribute('focused', `${true}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StoreEntry} entry
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_unfocusInvoker(entry) {
|
||||||
|
const { invoker } = entry;
|
||||||
|
const firstChild = invoker.firstElementChild;
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.removeAttribute('focused');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StoreEntry} entry
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_collapse(entry) {
|
||||||
|
const { content, invoker } = entry;
|
||||||
|
content.removeAttribute('expanded');
|
||||||
|
invoker.removeAttribute('expanded');
|
||||||
|
const firstChild = invoker.firstElementChild;
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.removeAttribute('expanded');
|
||||||
|
firstChild.setAttribute('aria-expanded', `${false}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StoreEntry} entry
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_expand(entry) {
|
||||||
|
const { content, invoker } = entry;
|
||||||
|
content.setAttribute('expanded', `${true}`);
|
||||||
|
invoker.setAttribute('expanded', `${true}`);
|
||||||
|
const firstChild = invoker.firstElementChild;
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.setAttribute('expanded', `${true}`);
|
||||||
|
firstChild.setAttribute('aria-expanded', `${true}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
__updateFocused() {
|
__updateFocused() {
|
||||||
if (!(this.__store && this.__store[this.focusedIndex])) {
|
const focusedEntry = this.__store[this.focusedIndex];
|
||||||
return;
|
const previousFocusedEntry = Array.from(this.__store).find(
|
||||||
}
|
entry => entry.invoker && entry.invoker.firstElementChild?.hasAttribute('focused'),
|
||||||
const previousInvoker = /** @type {HTMLElement | undefined} */ (
|
|
||||||
Array.from(this.children).find(
|
|
||||||
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
if (previousInvoker) {
|
if (previousFocusedEntry) {
|
||||||
unfocusInvoker(previousInvoker);
|
this._unfocusInvoker(previousFocusedEntry);
|
||||||
}
|
}
|
||||||
const { invoker: currentInvoker } = this.__store[this.focusedIndex];
|
if (focusedEntry) {
|
||||||
if (currentInvoker) {
|
this._focusInvoker(focusedEntry);
|
||||||
focusInvoker(currentInvoker);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -381,11 +363,9 @@ export class LionAccordion extends LitElement {
|
||||||
const entryExpanded = this.expanded.indexOf(index) !== -1;
|
const entryExpanded = this.expanded.indexOf(index) !== -1;
|
||||||
|
|
||||||
if (entryExpanded) {
|
if (entryExpanded) {
|
||||||
expandInvoker(entry.invoker);
|
this._expand(entry);
|
||||||
expandContent(entry.content);
|
|
||||||
} else {
|
} else {
|
||||||
collapseInvoker(entry.invoker);
|
this._collapse(entry);
|
||||||
collapseContent(entry.content);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -406,4 +386,17 @@ export class LionAccordion extends LitElement {
|
||||||
|
|
||||||
this.expanded = expanded;
|
this.expanded = expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
__cleanStore() {
|
||||||
|
if (!this.__store) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.__store.forEach(entry => {
|
||||||
|
this._cleanInvoker(entry);
|
||||||
|
});
|
||||||
|
this.__store = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue