feat(accordion): add types for accordion
This commit is contained in:
parent
56cc174c0b
commit
175e6bea6f
4 changed files with 206 additions and 105 deletions
5
.changeset/thin-rocks-chew.md
Normal file
5
.changeset/thin-rocks-chew.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/accordion': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added types for the accordion package.
|
||||||
|
|
@ -1,53 +1,118 @@
|
||||||
import { LitElement, css, html } from '@lion/core';
|
import { LitElement, css, html } from '@lion/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} StoreEntry
|
||||||
|
* @property {string} uid Unique ID for the entry
|
||||||
|
* @property {number} index index of the node
|
||||||
|
* @property {HTMLElement} invoker invoker node
|
||||||
|
* @property {HTMLElement} content content node
|
||||||
|
* @property {EventHandlerNonNull} clickHandler executed on click event
|
||||||
|
* @property {EventHandlerNonNull} keydownHandler executed on keydown event
|
||||||
|
*/
|
||||||
|
|
||||||
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 }) => {
|
const setupContent = ({ element, uid, index }) => {
|
||||||
element.style.setProperty('order', index + 1);
|
element.style.setProperty('order', `${index + 1}`);
|
||||||
element.setAttribute('id', `content-${uid}`);
|
element.setAttribute('id', `content-${uid}`);
|
||||||
element.setAttribute('aria-labelledby', `invoker-${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 }) => {
|
const setupInvoker = ({ element, uid, index, clickHandler, keydownHandler }) => {
|
||||||
element.style.setProperty('order', index + 1);
|
element.style.setProperty('order', `${index + 1}`);
|
||||||
element.firstElementChild.setAttribute('id', `invoker-${uid}`);
|
const firstChild = element.firstElementChild;
|
||||||
element.firstElementChild.setAttribute('aria-controls', `content-${uid}`);
|
if (firstChild) {
|
||||||
element.firstElementChild.addEventListener('click', clickHandler);
|
firstChild.setAttribute('id', `invoker-${uid}`);
|
||||||
element.firstElementChild.addEventListener('keydown', keydownHandler);
|
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 cleanInvoker = (element, clickHandler, keydownHandler) => {
|
||||||
element.firstElementChild.removeAttribute('id');
|
const firstChild = element.firstElementChild;
|
||||||
element.firstElementChild.removeAttribute('aria-controls');
|
if (firstChild) {
|
||||||
element.firstElementChild.removeEventListener('click', clickHandler);
|
firstChild.removeAttribute('id');
|
||||||
element.firstElementChild.removeEventListener('keydown', keydownHandler);
|
firstChild.removeAttribute('aria-controls');
|
||||||
|
firstChild.removeEventListener('click', clickHandler);
|
||||||
|
firstChild.removeEventListener('keydown', keydownHandler);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
const focusInvoker = element => {
|
const focusInvoker = element => {
|
||||||
element.firstElementChild.focus();
|
const firstChild = /** @type {HTMLElement|null} */ (element.firstElementChild);
|
||||||
element.firstElementChild.setAttribute('focused', true);
|
if (firstChild) {
|
||||||
|
firstChild.focus();
|
||||||
|
firstChild.setAttribute('focused', `${true}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
const unfocusInvoker = element => {
|
const unfocusInvoker = element => {
|
||||||
element.firstElementChild.removeAttribute('focused');
|
const firstChild = element.firstElementChild;
|
||||||
|
if (firstChild) {
|
||||||
|
firstChild.removeAttribute('focused');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
const expandInvoker = element => {
|
const expandInvoker = element => {
|
||||||
element.setAttribute('expanded', true);
|
element.setAttribute('expanded', `${true}`);
|
||||||
element.firstElementChild.setAttribute('expanded', true);
|
const firstChild = element.firstElementChild;
|
||||||
element.firstElementChild.setAttribute('aria-expanded', true);
|
if (firstChild) {
|
||||||
|
firstChild.setAttribute('expanded', `${true}`);
|
||||||
|
firstChild.setAttribute('aria-expanded', `${true}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
const collapseInvoker = element => {
|
const collapseInvoker = element => {
|
||||||
element.removeAttribute('expanded');
|
element.removeAttribute('expanded');
|
||||||
element.firstElementChild.removeAttribute('expanded');
|
const firstChild = element.firstElementChild;
|
||||||
element.firstElementChild.setAttribute('aria-expanded', false);
|
if (firstChild) {
|
||||||
|
firstChild.removeAttribute('expanded');
|
||||||
|
firstChild.setAttribute('aria-expanded', `${false}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
const expandContent = element => {
|
const expandContent = element => {
|
||||||
element.setAttribute('expanded', true);
|
element.setAttribute('expanded', `${true}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
const collapseContent = element => {
|
const collapseContent = element => {
|
||||||
element.removeAttribute('expanded');
|
element.removeAttribute('expanded');
|
||||||
};
|
};
|
||||||
|
|
@ -117,31 +182,44 @@ export class LionAccordion extends LitElement {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.focusedIndex = null;
|
|
||||||
this.expanded = [];
|
|
||||||
this.styles = {};
|
this.styles = {};
|
||||||
|
|
||||||
|
/** @type {StoreEntry[]} */
|
||||||
|
this.__store = [];
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
this.__focusedIndex = -1;
|
||||||
|
|
||||||
|
/** @type {number[]} */
|
||||||
|
this.__expanded = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||||
super.firstUpdated();
|
firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
this.__setupSlots();
|
this.__setupSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
__setupSlots() {
|
__setupSlots() {
|
||||||
const invokerSlot = this.shadowRoot.querySelector('slot[name=invoker]');
|
const invokerSlot = this.shadowRoot?.querySelector('slot[name=invoker]');
|
||||||
const handleSlotChange = () => {
|
const handleSlotChange = () => {
|
||||||
this.__cleanStore();
|
this.__cleanStore();
|
||||||
this.__setupStore();
|
this.__setupStore();
|
||||||
this.__updateFocused();
|
this.__updateFocused();
|
||||||
this.__updateExpanded();
|
this.__updateExpanded();
|
||||||
};
|
};
|
||||||
invokerSlot.addEventListener('slotchange', handleSlotChange);
|
if (invokerSlot) {
|
||||||
|
invokerSlot.addEventListener('slotchange', handleSlotChange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__setupStore() {
|
__setupStore() {
|
||||||
this.__store = [];
|
const invokers = /** @type {HTMLElement[]} */ (Array.from(
|
||||||
const invokers = this.querySelectorAll('[slot="invoker"]');
|
this.querySelectorAll('[slot="invoker"]'),
|
||||||
const contents = this.querySelectorAll('[slot="content"]');
|
));
|
||||||
|
const contents = /** @type {HTMLElement[]} */ (Array.from(
|
||||||
|
this.querySelectorAll('[slot="content"]'),
|
||||||
|
));
|
||||||
if (invokers.length !== contents.length) {
|
if (invokers.length !== contents.length) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|
@ -152,6 +230,7 @@ export class LionAccordion extends LitElement {
|
||||||
invokers.forEach((invoker, index) => {
|
invokers.forEach((invoker, index) => {
|
||||||
const uid = uuid();
|
const uid = uuid();
|
||||||
const content = contents[index];
|
const content = contents[index];
|
||||||
|
/** @type {StoreEntry} */
|
||||||
const entry = {
|
const entry = {
|
||||||
uid,
|
uid,
|
||||||
index,
|
index,
|
||||||
|
|
@ -176,8 +255,13 @@ export class LionAccordion extends LitElement {
|
||||||
this.__store.forEach(entry => {
|
this.__store.forEach(entry => {
|
||||||
cleanInvoker(entry.invoker, entry.clickHandler, entry.keydownHandler);
|
cleanInvoker(entry.invoker, entry.clickHandler, entry.keydownHandler);
|
||||||
});
|
});
|
||||||
|
this.__store = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
__createInvokerClickHandler(index) {
|
__createInvokerClickHandler(index) {
|
||||||
return () => {
|
return () => {
|
||||||
this.focusedIndex = index;
|
this.focusedIndex = index;
|
||||||
|
|
@ -185,28 +269,32 @@ export class LionAccordion extends LitElement {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
__handleInvokerKeydown(e) {
|
__handleInvokerKeydown(e) {
|
||||||
switch (e.key) {
|
const _e = /** @type {KeyboardEvent} */ (e);
|
||||||
|
switch (_e.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
e.preventDefault();
|
_e.preventDefault();
|
||||||
if (this.focusedIndex + 2 <= this._pairCount) {
|
if (this.focusedIndex + 2 <= this._pairCount) {
|
||||||
this.focusedIndex += 1;
|
this.focusedIndex += 1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
e.preventDefault();
|
_e.preventDefault();
|
||||||
if (this.focusedIndex >= 1) {
|
if (this.focusedIndex >= 1) {
|
||||||
this.focusedIndex -= 1;
|
this.focusedIndex -= 1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Home':
|
case 'Home':
|
||||||
e.preventDefault();
|
_e.preventDefault();
|
||||||
this.focusedIndex = 0;
|
this.focusedIndex = 0;
|
||||||
break;
|
break;
|
||||||
case 'End':
|
case 'End':
|
||||||
e.preventDefault();
|
_e.preventDefault();
|
||||||
this.focusedIndex = this._pairCount - 1;
|
this.focusedIndex = this._pairCount - 1;
|
||||||
break;
|
break;
|
||||||
/* no default */
|
/* no default */
|
||||||
|
|
@ -245,9 +333,9 @@ export class LionAccordion extends LitElement {
|
||||||
if (!(this.__store && this.__store[this.focusedIndex])) {
|
if (!(this.__store && this.__store[this.focusedIndex])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previousInvoker = Array.from(this.children).find(
|
const previousInvoker = /** @type {HTMLElement | null} */ (Array.from(this.children).find(
|
||||||
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
|
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
||||||
);
|
));
|
||||||
if (previousInvoker) {
|
if (previousInvoker) {
|
||||||
unfocusInvoker(previousInvoker);
|
unfocusInvoker(previousInvoker);
|
||||||
}
|
}
|
||||||
|
|
@ -274,6 +362,9 @@ export class LionAccordion extends LitElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
*/
|
||||||
__toggleExpanded(value) {
|
__toggleExpanded(value) {
|
||||||
const { expanded } = this;
|
const { expanded } = this;
|
||||||
const index = expanded.indexOf(value);
|
const index = expanded.indexOf(value);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import { expect, fixture, html } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import '../lion-accordion.js';
|
import '../lion-accordion.js';
|
||||||
|
/**
|
||||||
|
* @typedef {import('../src/LionAccordion.js').LionAccordion} LionAccordion
|
||||||
|
*/
|
||||||
|
|
||||||
const basicAccordion = html`
|
const basicAccordion = html`
|
||||||
<lion-accordion>
|
<lion-accordion>
|
||||||
|
|
@ -17,36 +20,36 @@ const basicAccordion = html`
|
||||||
describe('<lion-accordion>', () => {
|
describe('<lion-accordion>', () => {
|
||||||
describe('Accordion', () => {
|
describe('Accordion', () => {
|
||||||
it('sets expanded to [] by default', async () => {
|
it('sets expanded to [] by default', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
expect(el.expanded).to.deep.equal([]);
|
expect(el.expanded).to.deep.equal([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can programmatically set expanded', async () => {
|
it('can programmatically set expanded', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion .expanded=${[1]}>
|
<lion-accordion .expanded=${[1]}>
|
||||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||||
<div slot="content">content 1</div>
|
<div slot="content">content 1</div>
|
||||||
<h2 slot="invoker"><button>invoker 2</button></h2>
|
<h2 slot="invoker"><button>invoker 2</button></h2>
|
||||||
<div slot="content">content 2</div>
|
<div slot="content">content 2</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
expect(el.expanded).to.deep.equal([1]);
|
expect(el.expanded).to.deep.equal([1]);
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(
|
Array.from(el.children).find(
|
||||||
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
||||||
).textContent,
|
)?.textContent,
|
||||||
).to.equal('invoker 2');
|
).to.equal('invoker 2');
|
||||||
|
|
||||||
el.expanded = [0];
|
el.expanded = [0];
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(
|
Array.from(el.children).find(
|
||||||
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
||||||
).textContent,
|
)?.textContent,
|
||||||
).to.equal('invoker 1');
|
).to.equal('invoker 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has [expanded] on current expanded invoker which serves as styling hook', async () => {
|
it('has [expanded] on current expanded invoker which serves as styling hook', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
el.expanded = [0];
|
el.expanded = [0];
|
||||||
expect(invokers[0]).to.have.attribute('expanded');
|
expect(invokers[0]).to.have.attribute('expanded');
|
||||||
|
|
@ -58,7 +61,7 @@ describe('<lion-accordion>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has [expanded] on current expanded invoker first child which serves as styling hook', async () => {
|
it('has [expanded] on current expanded invoker first child which serves as styling hook', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
el.expanded = [0];
|
el.expanded = [0];
|
||||||
expect(invokers[0].firstElementChild).to.have.attribute('expanded');
|
expect(invokers[0].firstElementChild).to.have.attribute('expanded');
|
||||||
|
|
@ -70,7 +73,7 @@ describe('<lion-accordion>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends event "expanded-changed" for every expanded state change', async () => {
|
it('sends event "expanded-changed" for every expanded state change', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
el.addEventListener('expanded-changed', spy);
|
el.addEventListener('expanded-changed', spy);
|
||||||
el.expanded = [1];
|
el.expanded = [1];
|
||||||
|
|
@ -95,44 +98,44 @@ describe('<lion-accordion>', () => {
|
||||||
|
|
||||||
describe('Accordion navigation', () => {
|
describe('Accordion navigation', () => {
|
||||||
it('sets focusedIndex to null by default', async () => {
|
it('sets focusedIndex to null by default', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
expect(el.focusedIndex).to.be.null;
|
expect(el.focusedIndex).to.equal(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can programmatically set focusedIndex', async () => {
|
it('can programmatically set focusedIndex', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion .focusedIndex=${1}>
|
<lion-accordion .focusedIndex=${1}>
|
||||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||||
<div slot="content">content 1</div>
|
<div slot="content">content 1</div>
|
||||||
<h2 slot="invoker"><button>invoker 2</button></h2>
|
<h2 slot="invoker"><button>invoker 2</button></h2>
|
||||||
<div slot="content">content 2</div>
|
<div slot="content">content 2</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
expect(el.focusedIndex).to.equal(1);
|
expect(el.focusedIndex).to.equal(1);
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(
|
Array.from(el.children).find(
|
||||||
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
|
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
||||||
).textContent,
|
)?.textContent,
|
||||||
).to.equal('invoker 2');
|
).to.equal('invoker 2');
|
||||||
|
|
||||||
el.focusedIndex = 0;
|
el.focusedIndex = 0;
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(
|
Array.from(el.children).find(
|
||||||
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
|
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
||||||
).textContent,
|
)?.textContent,
|
||||||
).to.equal('invoker 1');
|
).to.equal('invoker 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has [focused] on current focused invoker first child which serves as styling hook', async () => {
|
it('has [focused] on current focused invoker first child which serves as styling hook', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
el.focusedIndex = [0];
|
el.focusedIndex = 0;
|
||||||
expect(invokers[0]).to.not.have.attribute('focused');
|
expect(invokers[0]).to.not.have.attribute('focused');
|
||||||
expect(invokers[1]).to.not.have.attribute('focused');
|
expect(invokers[1]).to.not.have.attribute('focused');
|
||||||
expect(invokers[0].firstElementChild).to.have.attribute('focused');
|
expect(invokers[0].firstElementChild).to.have.attribute('focused');
|
||||||
expect(invokers[1].firstElementChild).to.not.have.attribute('focused');
|
expect(invokers[1].firstElementChild).to.not.have.attribute('focused');
|
||||||
|
|
||||||
el.focusedIndex = [1];
|
el.focusedIndex = 1;
|
||||||
expect(invokers[0]).to.not.have.attribute('focused');
|
expect(invokers[0]).to.not.have.attribute('focused');
|
||||||
expect(invokers[1]).to.not.have.attribute('focused');
|
expect(invokers[1]).to.not.have.attribute('focused');
|
||||||
expect(invokers[0].firstElementChild).to.not.have.attribute('focused');
|
expect(invokers[0].firstElementChild).to.not.have.attribute('focused');
|
||||||
|
|
@ -140,7 +143,7 @@ describe('<lion-accordion>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends event "focused-changed" for every focused state change', async () => {
|
it('sends event "focused-changed" for every focused state change', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
el.addEventListener('focused-changed', spy);
|
el.addEventListener('focused-changed', spy);
|
||||||
el.focusedIndex = 1;
|
el.focusedIndex = 1;
|
||||||
|
|
@ -150,7 +153,7 @@ describe('<lion-accordion>', () => {
|
||||||
|
|
||||||
describe('Accordion Contents (slot=content)', () => {
|
describe('Accordion Contents (slot=content)', () => {
|
||||||
it('are visible when corresponding invoker is expanded', async () => {
|
it('are visible when corresponding invoker is expanded', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const contents = el.querySelectorAll('[slot=content]');
|
const contents = el.querySelectorAll('[slot=content]');
|
||||||
el.expanded = [0];
|
el.expanded = [0];
|
||||||
expect(contents[0]).to.be.visible;
|
expect(contents[0]).to.be.visible;
|
||||||
|
|
@ -174,44 +177,44 @@ describe('<lion-accordion>', () => {
|
||||||
*/
|
*/
|
||||||
describe('User interaction', () => {
|
describe('User interaction', () => {
|
||||||
it('opens a invoker on click', async () => {
|
it('opens a invoker on click', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[1].firstElementChild.dispatchEvent(new Event('click'));
|
invokers[1].firstElementChild?.dispatchEvent(new Event('click'));
|
||||||
expect(el.expanded).to.deep.equal([1]);
|
expect(el.expanded).to.deep.equal([1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects a invoker on click', async () => {
|
it('selects a invoker on click', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[1].firstElementChild.dispatchEvent(new Event('click'));
|
invokers[1].firstElementChild?.dispatchEvent(new Event('click'));
|
||||||
expect(el.focusedIndex).to.equal(1);
|
expect(el.focusedIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('opens/close invoker on [enter] and [space]', async () => {
|
it.skip('opens/close invoker on [enter] and [space]', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[0].firstElementChild.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
invokers[0].firstElementChild?.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||||
expect(el.expanded).to.deep.equal([0]);
|
expect(el.expanded).to.deep.equal([0]);
|
||||||
invokers[0].firstElementChild.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
invokers[0].firstElementChild?.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||||
expect(el.expanded).to.deep.equal([]);
|
expect(el.expanded).to.deep.equal([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects next invoker on [arrow-right] and [arrow-down]', async () => {
|
it('selects next invoker on [arrow-right] and [arrow-down]', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
el.focusedIndex = 0;
|
el.focusedIndex = 0;
|
||||||
invokers[0].firstElementChild.dispatchEvent(
|
invokers[0].firstElementChild?.dispatchEvent(
|
||||||
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
|
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
|
||||||
);
|
);
|
||||||
expect(el.focusedIndex).to.equal(1);
|
expect(el.focusedIndex).to.equal(1);
|
||||||
invokers[0].firstElementChild.dispatchEvent(
|
invokers[0].firstElementChild?.dispatchEvent(
|
||||||
new KeyboardEvent('keydown', { key: 'ArrowDown' }),
|
new KeyboardEvent('keydown', { key: 'ArrowDown' }),
|
||||||
);
|
);
|
||||||
expect(el.focusedIndex).to.equal(2);
|
expect(el.focusedIndex).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects previous invoker on [arrow-left] and [arrow-up]', async () => {
|
it('selects previous invoker on [arrow-left] and [arrow-up]', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion .focusedIndex=${1}>
|
<lion-accordion .focusedIndex=${1}>
|
||||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||||
<div slot="content">content 1</div>
|
<div slot="content">content 1</div>
|
||||||
|
|
@ -220,40 +223,42 @@ describe('<lion-accordion>', () => {
|
||||||
<h2 slot="invoker"><button>invoker 3</button></h2>
|
<h2 slot="invoker"><button>invoker 3</button></h2>
|
||||||
<div slot="content">content 3</div>
|
<div slot="content">content 3</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
el.focusedIndex = 2;
|
el.focusedIndex = 2;
|
||||||
invokers[2].firstElementChild.dispatchEvent(
|
invokers[2].firstElementChild?.dispatchEvent(
|
||||||
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
|
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
|
||||||
);
|
);
|
||||||
expect(el.focusedIndex).to.equal(1);
|
expect(el.focusedIndex).to.equal(1);
|
||||||
invokers[1].firstElementChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
|
invokers[1].firstElementChild?.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', { key: 'ArrowUp' }),
|
||||||
|
);
|
||||||
expect(el.focusedIndex).to.equal(0);
|
expect(el.focusedIndex).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects first invoker on [home]', async () => {
|
it('selects first invoker on [home]', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion .focusedIndex=${1}>
|
<lion-accordion .focusedIndex=${1}>
|
||||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||||
<div slot="content">content 1</div>
|
<div slot="content">content 1</div>
|
||||||
<h2 slot="invoker"><button>invoker 2</button></h2>
|
<h2 slot="invoker"><button>invoker 2</button></h2>
|
||||||
<div slot="content">content 2</div>
|
<div slot="content">content 2</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[1].firstElementChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
|
invokers[1].firstElementChild?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home' }));
|
||||||
expect(el.focusedIndex).to.equal(0);
|
expect(el.focusedIndex).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects last invoker on [end]', async () => {
|
it('selects last invoker on [end]', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[0].firstElementChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' }));
|
invokers[0].firstElementChild?.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' }));
|
||||||
expect(el.focusedIndex).to.equal(2);
|
expect(el.focusedIndex).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stays on last invoker on [arrow-right]', async () => {
|
it('stays on last invoker on [arrow-right]', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion focusedIndex="2">
|
<lion-accordion focusedIndex="2">
|
||||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||||
<div slot="content">content 1</div>
|
<div slot="content">content 1</div>
|
||||||
|
|
@ -262,16 +267,16 @@ describe('<lion-accordion>', () => {
|
||||||
<h2 slot="invoker"><button>invoker 3</button></h2>
|
<h2 slot="invoker"><button>invoker 3</button></h2>
|
||||||
<div slot="content">content 3</div>
|
<div slot="content">content 3</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[2].firstElementChild.dispatchEvent(
|
invokers[2].firstElementChild?.dispatchEvent(
|
||||||
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
|
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
|
||||||
);
|
);
|
||||||
expect(el.focusedIndex).to.equal(2);
|
expect(el.focusedIndex).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stays on first invoker on [arrow-left]', async () => {
|
it('stays on first invoker on [arrow-left]', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion>
|
<lion-accordion>
|
||||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||||
<div slot="content">content 1</div>
|
<div slot="content">content 1</div>
|
||||||
|
|
@ -280,21 +285,19 @@ describe('<lion-accordion>', () => {
|
||||||
<h2 slot="invoker"><button>invoker 3</button></h2>
|
<h2 slot="invoker"><button>invoker 3</button></h2>
|
||||||
<div slot="content">content 3</div>
|
<div slot="content">content 3</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
invokers[0].firstElementChild.dispatchEvent(
|
invokers[0].firstElementChild?.dispatchEvent(
|
||||||
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
|
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
|
||||||
);
|
);
|
||||||
expect(el.focusedIndex).to.equal(null);
|
expect(el.focusedIndex).to.equal(-1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Content distribution', () => {
|
describe('Content distribution', () => {
|
||||||
it('should work with append children', async () => {
|
it('should work with append children', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const c = 2;
|
for (let i = 4; i < 6; i += 1) {
|
||||||
const n = el.children.length / 2;
|
|
||||||
for (let i = n + 1; i < n + c + 1; i += 1) {
|
|
||||||
const invoker = document.createElement('h2');
|
const invoker = document.createElement('h2');
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
invoker.setAttribute('slot', 'invoker');
|
invoker.setAttribute('slot', 'invoker');
|
||||||
|
|
@ -306,25 +309,23 @@ describe('<lion-accordion>', () => {
|
||||||
el.append(invoker);
|
el.append(invoker);
|
||||||
el.append(content);
|
el.append(content);
|
||||||
}
|
}
|
||||||
el.expanded = [el.children.length / 2 - 1];
|
el.expanded = [4];
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(
|
Array.from(el.children).find(
|
||||||
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
||||||
).textContent,
|
)?.textContent,
|
||||||
).to.equal('invoker 5');
|
).to.equal('invoker 5');
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(
|
Array.from(el.children).find(
|
||||||
child => child.slot === 'content' && child.hasAttribute('expanded'),
|
child => child.slot === 'content' && child.hasAttribute('expanded'),
|
||||||
).textContent,
|
)?.textContent,
|
||||||
).to.equal('content 5');
|
).to.equal('content 5');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add order style property to each invoker and content', async () => {
|
it('should add order style property to each invoker and content', async () => {
|
||||||
const el = await fixture(basicAccordion);
|
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||||
const c = 2;
|
for (let i = 4; i < 6; i += 1) {
|
||||||
const n = el.children.length / 2;
|
|
||||||
for (let i = n + 1; i < n + c + 1; i += 1) {
|
|
||||||
const invoker = document.createElement('h2');
|
const invoker = document.createElement('h2');
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
invoker.setAttribute('slot', 'invoker');
|
invoker.setAttribute('slot', 'invoker');
|
||||||
|
|
@ -336,10 +337,13 @@ describe('<lion-accordion>', () => {
|
||||||
el.append(invoker);
|
el.append(invoker);
|
||||||
el.append(content);
|
el.append(content);
|
||||||
}
|
}
|
||||||
el.expanded = [el.children.length / 2 - 1];
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = /** @type {HTMLElement[]} */ (Array.from(
|
||||||
const contents = el.querySelectorAll('[slot=content]');
|
el.querySelectorAll('[slot=invoker]'),
|
||||||
|
));
|
||||||
|
const contents = /** @type {HTMLElement[]} */ (Array.from(
|
||||||
|
el.querySelectorAll('[slot=content]'),
|
||||||
|
));
|
||||||
invokers.forEach((invoker, index) => {
|
invokers.forEach((invoker, index) => {
|
||||||
const content = contents[index];
|
const content = contents[index];
|
||||||
expect(invoker.style.getPropertyValue('order')).to.equal(`${index + 1}`);
|
expect(invoker.style.getPropertyValue('order')).to.equal(`${index + 1}`);
|
||||||
|
|
@ -378,10 +382,10 @@ describe('<lion-accordion>', () => {
|
||||||
`);
|
`);
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
const contents = el.querySelectorAll('[slot=content]');
|
const contents = el.querySelectorAll('[slot=content]');
|
||||||
expect(invokers[0].firstElementChild.getAttribute('aria-controls')).to.equal(
|
expect(invokers[0].firstElementChild?.getAttribute('aria-controls')).to.equal(
|
||||||
contents[0].id,
|
contents[0].id,
|
||||||
);
|
);
|
||||||
expect(invokers[1].firstElementChild.getAttribute('aria-controls')).to.equal(
|
expect(invokers[1].firstElementChild?.getAttribute('aria-controls')).to.equal(
|
||||||
contents[1].id,
|
contents[1].id,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -394,20 +398,20 @@ describe('<lion-accordion>', () => {
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`);
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(child => child.slot === 'invoker').firstElementChild,
|
Array.from(el.children).find(child => child.slot === 'invoker')?.firstElementChild,
|
||||||
).to.have.attribute('aria-expanded', 'false');
|
).to.have.attribute('aria-expanded', 'false');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds aria-expanded="true" to invoker when its content is expanded', async () => {
|
it('adds aria-expanded="true" to invoker when its content is expanded', async () => {
|
||||||
const el = await fixture(html`
|
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||||
<lion-accordion>
|
<lion-accordion>
|
||||||
<h2 slot="invoker"><button>invoker</button></h2>
|
<h2 slot="invoker"><button>invoker</button></h2>
|
||||||
<div slot="content">content</div>
|
<div slot="content">content</div>
|
||||||
</lion-accordion>
|
</lion-accordion>
|
||||||
`);
|
`));
|
||||||
el.expanded = [0];
|
el.expanded = [0];
|
||||||
expect(
|
expect(
|
||||||
Array.from(el.children).find(child => child.slot === 'invoker').firstElementChild,
|
Array.from(el.children).find(child => child.slot === 'invoker')?.firstElementChild,
|
||||||
).to.have.attribute('aria-expanded', 'true');
|
).to.have.attribute('aria-expanded', 'true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -424,8 +428,8 @@ describe('<lion-accordion>', () => {
|
||||||
`);
|
`);
|
||||||
const contents = el.querySelectorAll('[slot=content]');
|
const contents = el.querySelectorAll('[slot=content]');
|
||||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||||
expect(contents[0]).to.have.attribute('aria-labelledby', invokers[0].firstElementChild.id);
|
expect(contents[0]).to.have.attribute('aria-labelledby', invokers[0].firstElementChild?.id);
|
||||||
expect(contents[1]).to.have.attribute('aria-labelledby', invokers[1].firstElementChild.id);
|
expect(contents[1]).to.have.attribute('aria-labelledby', invokers[1].firstElementChild?.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"suppressImplicitAnyIndexErrors": true
|
"suppressImplicitAnyIndexErrors": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"packages/accordion/**/*.js",
|
||||||
"packages/core/**/*.js",
|
"packages/core/**/*.js",
|
||||||
"packages/tabs/**/*.js",
|
"packages/tabs/**/*.js",
|
||||||
"packages/singleton-manager/**/*.js",
|
"packages/singleton-manager/**/*.js",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue