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';
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @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.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);
|
||||
element.firstElementChild.setAttribute('id', `invoker-${uid}`);
|
||||
element.firstElementChild.setAttribute('aria-controls', `content-${uid}`);
|
||||
element.firstElementChild.addEventListener('click', clickHandler);
|
||||
element.firstElementChild.addEventListener('keydown', 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) => {
|
||||
element.firstElementChild.removeAttribute('id');
|
||||
element.firstElementChild.removeAttribute('aria-controls');
|
||||
element.firstElementChild.removeEventListener('click', clickHandler);
|
||||
element.firstElementChild.removeEventListener('keydown', 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 => {
|
||||
element.firstElementChild.focus();
|
||||
element.firstElementChild.setAttribute('focused', true);
|
||||
const firstChild = /** @type {HTMLElement|null} */ (element.firstElementChild);
|
||||
if (firstChild) {
|
||||
firstChild.focus();
|
||||
firstChild.setAttribute('focused', `${true}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
const unfocusInvoker = element => {
|
||||
element.firstElementChild.removeAttribute('focused');
|
||||
const firstChild = element.firstElementChild;
|
||||
if (firstChild) {
|
||||
firstChild.removeAttribute('focused');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
const expandInvoker = element => {
|
||||
element.setAttribute('expanded', true);
|
||||
element.firstElementChild.setAttribute('expanded', true);
|
||||
element.firstElementChild.setAttribute('aria-expanded', true);
|
||||
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');
|
||||
element.firstElementChild.removeAttribute('expanded');
|
||||
element.firstElementChild.setAttribute('aria-expanded', false);
|
||||
const firstChild = element.firstElementChild;
|
||||
if (firstChild) {
|
||||
firstChild.removeAttribute('expanded');
|
||||
firstChild.setAttribute('aria-expanded', `${false}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
const expandContent = element => {
|
||||
element.setAttribute('expanded', true);
|
||||
element.setAttribute('expanded', `${true}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
const collapseContent = element => {
|
||||
element.removeAttribute('expanded');
|
||||
};
|
||||
|
|
@ -117,31 +182,44 @@ export class LionAccordion extends LitElement {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.focusedIndex = null;
|
||||
this.expanded = [];
|
||||
this.styles = {};
|
||||
|
||||
/** @type {StoreEntry[]} */
|
||||
this.__store = [];
|
||||
|
||||
/** @type {number} */
|
||||
this.__focusedIndex = -1;
|
||||
|
||||
/** @type {number[]} */
|
||||
this.__expanded = [];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
super.firstUpdated();
|
||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__setupSlots();
|
||||
}
|
||||
|
||||
__setupSlots() {
|
||||
const invokerSlot = this.shadowRoot.querySelector('slot[name=invoker]');
|
||||
const invokerSlot = this.shadowRoot?.querySelector('slot[name=invoker]');
|
||||
const handleSlotChange = () => {
|
||||
this.__cleanStore();
|
||||
this.__setupStore();
|
||||
this.__updateFocused();
|
||||
this.__updateExpanded();
|
||||
};
|
||||
invokerSlot.addEventListener('slotchange', handleSlotChange);
|
||||
if (invokerSlot) {
|
||||
invokerSlot.addEventListener('slotchange', handleSlotChange);
|
||||
}
|
||||
}
|
||||
|
||||
__setupStore() {
|
||||
this.__store = [];
|
||||
const invokers = this.querySelectorAll('[slot="invoker"]');
|
||||
const contents = this.querySelectorAll('[slot="content"]');
|
||||
const invokers = /** @type {HTMLElement[]} */ (Array.from(
|
||||
this.querySelectorAll('[slot="invoker"]'),
|
||||
));
|
||||
const contents = /** @type {HTMLElement[]} */ (Array.from(
|
||||
this.querySelectorAll('[slot="content"]'),
|
||||
));
|
||||
if (invokers.length !== contents.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
|
|
@ -152,6 +230,7 @@ export class LionAccordion extends LitElement {
|
|||
invokers.forEach((invoker, index) => {
|
||||
const uid = uuid();
|
||||
const content = contents[index];
|
||||
/** @type {StoreEntry} */
|
||||
const entry = {
|
||||
uid,
|
||||
index,
|
||||
|
|
@ -176,8 +255,13 @@ export class LionAccordion extends LitElement {
|
|||
this.__store.forEach(entry => {
|
||||
cleanInvoker(entry.invoker, entry.clickHandler, entry.keydownHandler);
|
||||
});
|
||||
this.__store = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
*/
|
||||
__createInvokerClickHandler(index) {
|
||||
return () => {
|
||||
this.focusedIndex = index;
|
||||
|
|
@ -185,28 +269,32 @@ export class LionAccordion extends LitElement {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} e
|
||||
*/
|
||||
__handleInvokerKeydown(e) {
|
||||
switch (e.key) {
|
||||
const _e = /** @type {KeyboardEvent} */ (e);
|
||||
switch (_e.key) {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
_e.preventDefault();
|
||||
if (this.focusedIndex + 2 <= this._pairCount) {
|
||||
this.focusedIndex += 1;
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
_e.preventDefault();
|
||||
if (this.focusedIndex >= 1) {
|
||||
this.focusedIndex -= 1;
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
_e.preventDefault();
|
||||
this.focusedIndex = 0;
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
_e.preventDefault();
|
||||
this.focusedIndex = this._pairCount - 1;
|
||||
break;
|
||||
/* no default */
|
||||
|
|
@ -245,9 +333,9 @@ export class LionAccordion extends LitElement {
|
|||
if (!(this.__store && this.__store[this.focusedIndex])) {
|
||||
return;
|
||||
}
|
||||
const previousInvoker = Array.from(this.children).find(
|
||||
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
|
||||
);
|
||||
const previousInvoker = /** @type {HTMLElement | null} */ (Array.from(this.children).find(
|
||||
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
||||
));
|
||||
if (previousInvoker) {
|
||||
unfocusInvoker(previousInvoker);
|
||||
}
|
||||
|
|
@ -274,6 +362,9 @@ export class LionAccordion extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
__toggleExpanded(value) {
|
||||
const { expanded } = this;
|
||||
const index = expanded.indexOf(value);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { expect, fixture, html } from '@open-wc/testing';
|
|||
import sinon from 'sinon';
|
||||
|
||||
import '../lion-accordion.js';
|
||||
/**
|
||||
* @typedef {import('../src/LionAccordion.js').LionAccordion} LionAccordion
|
||||
*/
|
||||
|
||||
const basicAccordion = html`
|
||||
<lion-accordion>
|
||||
|
|
@ -17,36 +20,36 @@ const basicAccordion = html`
|
|||
describe('<lion-accordion>', () => {
|
||||
describe('Accordion', () => {
|
||||
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([]);
|
||||
});
|
||||
|
||||
it('can programmatically set expanded', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||
<lion-accordion .expanded=${[1]}>
|
||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||
<div slot="content">content 1</div>
|
||||
<h2 slot="invoker"><button>invoker 2</button></h2>
|
||||
<div slot="content">content 2</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
expect(el.expanded).to.deep.equal([1]);
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
||||
).textContent,
|
||||
)?.textContent,
|
||||
).to.equal('invoker 2');
|
||||
|
||||
el.expanded = [0];
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
||||
).textContent,
|
||||
)?.textContent,
|
||||
).to.equal('invoker 1');
|
||||
});
|
||||
|
||||
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]');
|
||||
el.expanded = [0];
|
||||
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 () => {
|
||||
const el = await fixture(basicAccordion);
|
||||
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
el.expanded = [0];
|
||||
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 () => {
|
||||
const el = await fixture(basicAccordion);
|
||||
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||
const spy = sinon.spy();
|
||||
el.addEventListener('expanded-changed', spy);
|
||||
el.expanded = [1];
|
||||
|
|
@ -95,44 +98,44 @@ describe('<lion-accordion>', () => {
|
|||
|
||||
describe('Accordion navigation', () => {
|
||||
it('sets focusedIndex to null by default', async () => {
|
||||
const el = await fixture(basicAccordion);
|
||||
expect(el.focusedIndex).to.be.null;
|
||||
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||
expect(el.focusedIndex).to.equal(-1);
|
||||
});
|
||||
|
||||
it('can programmatically set focusedIndex', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||
<lion-accordion .focusedIndex=${1}>
|
||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||
<div slot="content">content 1</div>
|
||||
<h2 slot="invoker"><button>invoker 2</button></h2>
|
||||
<div slot="content">content 2</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
expect(el.focusedIndex).to.equal(1);
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
|
||||
).textContent,
|
||||
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
||||
)?.textContent,
|
||||
).to.equal('invoker 2');
|
||||
|
||||
el.focusedIndex = 0;
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
child => child.slot === 'invoker' && child.firstElementChild.hasAttribute('focused'),
|
||||
).textContent,
|
||||
child => child.slot === 'invoker' && child.firstElementChild?.hasAttribute('focused'),
|
||||
)?.textContent,
|
||||
).to.equal('invoker 1');
|
||||
});
|
||||
|
||||
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]');
|
||||
el.focusedIndex = [0];
|
||||
el.focusedIndex = 0;
|
||||
expect(invokers[0]).to.not.have.attribute('focused');
|
||||
expect(invokers[1]).to.not.have.attribute('focused');
|
||||
expect(invokers[0].firstElementChild).to.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[1]).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 () => {
|
||||
const el = await fixture(basicAccordion);
|
||||
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||
const spy = sinon.spy();
|
||||
el.addEventListener('focused-changed', spy);
|
||||
el.focusedIndex = 1;
|
||||
|
|
@ -150,7 +153,7 @@ describe('<lion-accordion>', () => {
|
|||
|
||||
describe('Accordion Contents (slot=content)', () => {
|
||||
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]');
|
||||
el.expanded = [0];
|
||||
expect(contents[0]).to.be.visible;
|
||||
|
|
@ -174,44 +177,44 @@ describe('<lion-accordion>', () => {
|
|||
*/
|
||||
describe('User interaction', () => {
|
||||
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]');
|
||||
invokers[1].firstElementChild.dispatchEvent(new Event('click'));
|
||||
invokers[1].firstElementChild?.dispatchEvent(new Event('click'));
|
||||
expect(el.expanded).to.deep.equal([1]);
|
||||
});
|
||||
|
||||
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]');
|
||||
invokers[1].firstElementChild.dispatchEvent(new Event('click'));
|
||||
invokers[1].firstElementChild?.dispatchEvent(new Event('click'));
|
||||
expect(el.focusedIndex).to.equal(1);
|
||||
});
|
||||
|
||||
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]');
|
||||
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]);
|
||||
invokers[0].firstElementChild.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
invokers[0].firstElementChild?.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
expect(el.expanded).to.deep.equal([]);
|
||||
});
|
||||
|
||||
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]');
|
||||
el.focusedIndex = 0;
|
||||
invokers[0].firstElementChild.dispatchEvent(
|
||||
invokers[0].firstElementChild?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
|
||||
);
|
||||
expect(el.focusedIndex).to.equal(1);
|
||||
invokers[0].firstElementChild.dispatchEvent(
|
||||
invokers[0].firstElementChild?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown' }),
|
||||
);
|
||||
expect(el.focusedIndex).to.equal(2);
|
||||
});
|
||||
|
||||
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}>
|
||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||
<div slot="content">content 1</div>
|
||||
|
|
@ -220,40 +223,42 @@ describe('<lion-accordion>', () => {
|
|||
<h2 slot="invoker"><button>invoker 3</button></h2>
|
||||
<div slot="content">content 3</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
el.focusedIndex = 2;
|
||||
invokers[2].firstElementChild.dispatchEvent(
|
||||
invokers[2].firstElementChild?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
|
||||
);
|
||||
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);
|
||||
});
|
||||
|
||||
it('selects first invoker on [home]', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||
<lion-accordion .focusedIndex=${1}>
|
||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||
<div slot="content">content 1</div>
|
||||
<h2 slot="invoker"><button>invoker 2</button></h2>
|
||||
<div slot="content">content 2</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
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);
|
||||
});
|
||||
|
||||
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]');
|
||||
invokers[0].firstElementChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' }));
|
||||
invokers[0].firstElementChild?.dispatchEvent(new KeyboardEvent('keydown', { key: 'End' }));
|
||||
expect(el.focusedIndex).to.equal(2);
|
||||
});
|
||||
|
||||
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">
|
||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||
<div slot="content">content 1</div>
|
||||
|
|
@ -262,16 +267,16 @@ describe('<lion-accordion>', () => {
|
|||
<h2 slot="invoker"><button>invoker 3</button></h2>
|
||||
<div slot="content">content 3</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
invokers[2].firstElementChild.dispatchEvent(
|
||||
invokers[2].firstElementChild?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
|
||||
);
|
||||
expect(el.focusedIndex).to.equal(2);
|
||||
});
|
||||
|
||||
it('stays on first invoker on [arrow-left]', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionAccordion} */ (await fixture(html`
|
||||
<lion-accordion>
|
||||
<h2 slot="invoker"><button>invoker 1</button></h2>
|
||||
<div slot="content">content 1</div>
|
||||
|
|
@ -280,21 +285,19 @@ describe('<lion-accordion>', () => {
|
|||
<h2 slot="invoker"><button>invoker 3</button></h2>
|
||||
<div slot="content">content 3</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
invokers[0].firstElementChild.dispatchEvent(
|
||||
invokers[0].firstElementChild?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
|
||||
);
|
||||
expect(el.focusedIndex).to.equal(null);
|
||||
expect(el.focusedIndex).to.equal(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content distribution', () => {
|
||||
it('should work with append children', async () => {
|
||||
const el = await fixture(basicAccordion);
|
||||
const c = 2;
|
||||
const n = el.children.length / 2;
|
||||
for (let i = n + 1; i < n + c + 1; i += 1) {
|
||||
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||
for (let i = 4; i < 6; i += 1) {
|
||||
const invoker = document.createElement('h2');
|
||||
const button = document.createElement('button');
|
||||
invoker.setAttribute('slot', 'invoker');
|
||||
|
|
@ -306,25 +309,23 @@ describe('<lion-accordion>', () => {
|
|||
el.append(invoker);
|
||||
el.append(content);
|
||||
}
|
||||
el.expanded = [el.children.length / 2 - 1];
|
||||
el.expanded = [4];
|
||||
await el.updateComplete;
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
child => child.slot === 'invoker' && child.hasAttribute('expanded'),
|
||||
).textContent,
|
||||
)?.textContent,
|
||||
).to.equal('invoker 5');
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
child => child.slot === 'content' && child.hasAttribute('expanded'),
|
||||
).textContent,
|
||||
)?.textContent,
|
||||
).to.equal('content 5');
|
||||
});
|
||||
|
||||
it('should add order style property to each invoker and content', async () => {
|
||||
const el = await fixture(basicAccordion);
|
||||
const c = 2;
|
||||
const n = el.children.length / 2;
|
||||
for (let i = n + 1; i < n + c + 1; i += 1) {
|
||||
const el = /** @type {LionAccordion} */ (await fixture(basicAccordion));
|
||||
for (let i = 4; i < 6; i += 1) {
|
||||
const invoker = document.createElement('h2');
|
||||
const button = document.createElement('button');
|
||||
invoker.setAttribute('slot', 'invoker');
|
||||
|
|
@ -336,10 +337,13 @@ describe('<lion-accordion>', () => {
|
|||
el.append(invoker);
|
||||
el.append(content);
|
||||
}
|
||||
el.expanded = [el.children.length / 2 - 1];
|
||||
await el.updateComplete;
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
const contents = el.querySelectorAll('[slot=content]');
|
||||
const invokers = /** @type {HTMLElement[]} */ (Array.from(
|
||||
el.querySelectorAll('[slot=invoker]'),
|
||||
));
|
||||
const contents = /** @type {HTMLElement[]} */ (Array.from(
|
||||
el.querySelectorAll('[slot=content]'),
|
||||
));
|
||||
invokers.forEach((invoker, index) => {
|
||||
const content = contents[index];
|
||||
expect(invoker.style.getPropertyValue('order')).to.equal(`${index + 1}`);
|
||||
|
|
@ -378,10 +382,10 @@ describe('<lion-accordion>', () => {
|
|||
`);
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
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,
|
||||
);
|
||||
expect(invokers[1].firstElementChild.getAttribute('aria-controls')).to.equal(
|
||||
expect(invokers[1].firstElementChild?.getAttribute('aria-controls')).to.equal(
|
||||
contents[1].id,
|
||||
);
|
||||
});
|
||||
|
|
@ -394,20 +398,20 @@ describe('<lion-accordion>', () => {
|
|||
</lion-accordion>
|
||||
`);
|
||||
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');
|
||||
});
|
||||
|
||||
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>
|
||||
<h2 slot="invoker"><button>invoker</button></h2>
|
||||
<div slot="content">content</div>
|
||||
</lion-accordion>
|
||||
`);
|
||||
`));
|
||||
el.expanded = [0];
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
@ -424,8 +428,8 @@ describe('<lion-accordion>', () => {
|
|||
`);
|
||||
const contents = el.querySelectorAll('[slot=content]');
|
||||
const invokers = el.querySelectorAll('[slot=invoker]');
|
||||
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[0]).to.have.attribute('aria-labelledby', invokers[0].firstElementChild?.id);
|
||||
expect(contents[1]).to.have.attribute('aria-labelledby', invokers[1].firstElementChild?.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"suppressImplicitAnyIndexErrors": true
|
||||
},
|
||||
"include": [
|
||||
"packages/accordion/**/*.js",
|
||||
"packages/core/**/*.js",
|
||||
"packages/tabs/**/*.js",
|
||||
"packages/singleton-manager/**/*.js",
|
||||
|
|
|
|||
Loading…
Reference in a new issue