feat(tabs): fix types and export type definition files for tabs

This commit is contained in:
Joren Broekema 2020-07-13 11:30:55 +02:00
parent ec65da5da6
commit 0cf8a0c921
3 changed files with 238 additions and 157 deletions

View file

@ -13,11 +13,14 @@
"main": "index.js", "main": "index.js",
"module": "index.js", "module": "index.js",
"files": [ "files": [
"*.d.ts",
"*.js", "*.js",
"docs", "docs",
"src", "src",
"test", "test",
"translations" "test-helpers",
"translations",
"types"
], ],
"scripts": { "scripts": {
"prepublishOnly": "../../scripts/npm-prepublish.js", "prepublishOnly": "../../scripts/npm-prepublish.js",

View file

@ -1,77 +1,114 @@
import { css, html, LitElement } from '@lion/core'; import { css, html, LitElement } from '@lion/core';
const uuid = () => Math.random().toString(36).substr(2, 10); /**
* @typedef {Object} StoreEntry
* @property {HTMLElement} el Dom Element
* @property {string} uid Unique ID for the entry
* @property {HTMLElement} button Button HTMLElement for the entry
* @property {HTMLElement} panel Panel HTMLElement for the entry
* @property {EventHandlerNonNull} clickHandler executed on click event
* @property {EventHandlerNonNull} keydownHandler executed on keydown event
* @property {EventHandlerNonNull} keyupHandler executed on keyup event
*/
const setupPanel = ({ element, uid }) => { function uuid() {
element.setAttribute('id', `panel-${uid}`); return Math.random().toString(36).substr(2, 10);
element.setAttribute('role', 'tabpanel'); }
element.setAttribute('aria-labelledby', `button-${uid}`);
};
const selectPanel = element => { /**
element.setAttribute('selected', true); * @param {StoreEntry} options
}; */
function setupPanel({ el, uid }) {
el.setAttribute('id', `panel-${uid}`);
el.setAttribute('role', 'tabpanel');
el.setAttribute('aria-labelledby', `button-${uid}`);
}
const deselectPanel = element => { /**
element.removeAttribute('selected'); * @param {HTMLElement} el
}; */
function selectPanel(el) {
el.setAttribute('selected', 'true');
}
const setupButton = ({ element, uid, clickHandler, keydownHandler, keyupHandler }) => { /**
element.setAttribute('id', `button-${uid}`); * @param {HTMLElement} el
element.setAttribute('role', 'tab'); */
element.setAttribute('aria-controls', `panel-${uid}`); function deselectPanel(el) {
element.addEventListener('click', clickHandler); el.removeAttribute('selected');
element.addEventListener('keyup', keyupHandler); }
element.addEventListener('keydown', keydownHandler);
};
const cleanButton = ({ element, clickHandler, keydownHandler, keyupHandler }) => { /**
element.removeAttribute('id'); * @param {StoreEntry} options
element.removeAttribute('role'); */
element.removeAttribute('aria-controls'); function setupButton({ el, uid, clickHandler, keydownHandler, keyupHandler }) {
element.removeEventListener('click', clickHandler); el.setAttribute('id', `button-${uid}`);
element.removeEventListener('keyup', keyupHandler); el.setAttribute('role', 'tab');
element.removeEventListener('keydown', keydownHandler); el.setAttribute('aria-controls', `panel-${uid}`);
}; el.addEventListener('click', clickHandler);
el.addEventListener('keyup', keyupHandler);
el.addEventListener('keydown', keydownHandler);
}
const selectButton = (element, withFocus = false) => { /**
* @param {StoreEntry} options
*/
function cleanButton({ el, clickHandler, keydownHandler, keyupHandler }) {
el.removeAttribute('id');
el.removeAttribute('role');
el.removeAttribute('aria-controls');
el.removeEventListener('click', clickHandler);
el.removeEventListener('keyup', keyupHandler);
el.removeEventListener('keydown', keydownHandler);
}
/**
* @param {HTMLElement} el
* @param {boolean} withFocus
*/
function selectButton(el, withFocus = false) {
if (withFocus) { if (withFocus) {
element.focus(); el.focus();
} }
element.setAttribute('selected', true); el.setAttribute('selected', 'true');
element.setAttribute('aria-selected', true); el.setAttribute('aria-selected', 'true');
element.setAttribute('tabindex', 0); el.setAttribute('tabindex', '0');
}; }
const deselectButton = element => { /**
element.removeAttribute('selected'); * @param {HTMLElement} el
element.setAttribute('aria-selected', false); */
element.setAttribute('tabindex', -1); function deselectButton(el) {
}; el.removeAttribute('selected');
el.setAttribute('aria-selected', 'false');
el.setAttribute('tabindex', '-1');
}
const handleButtonKeydown = e => { /**
switch (e.key) { * @param {Event} ev
*/
function handleButtonKeydown(ev) {
const _ev = /** @type {KeyboardEvent} */ (ev);
switch (_ev.key) {
case 'ArrowDown': case 'ArrowDown':
case 'ArrowRight': case 'ArrowRight':
case 'ArrowUp': case 'ArrowUp':
case 'ArrowLeft': case 'ArrowLeft':
case 'Home': case 'Home':
case 'End': case 'End':
e.preventDefault(); _ev.preventDefault();
/* no default */ /* no default */
} }
}; }
export class LionTabs extends LitElement { export class LionTabs extends LitElement {
static get properties() { static get properties() {
return { return {
/**
* index number of the selected tab.
*/
selectedIndex: { selectedIndex: {
type: Number, type: Number,
value: 0, attribute: 'selected-index',
reflect: true,
}, },
}; };
} }
@ -117,28 +154,42 @@ export class LionTabs extends LitElement {
constructor() { constructor() {
super(); super();
/**
* An index number of the selected tab
*/
this.selectedIndex = 0; this.selectedIndex = 0;
} }
firstUpdated() { /** @param {import('lit-element').PropertyValues } changedProperties */
super.firstUpdated(); firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.__setupSlots(); this.__setupSlots();
} }
__setupSlots() { __setupSlots() {
if (this.shadowRoot) {
const tabSlot = this.shadowRoot.querySelector('slot[name=tab]'); const tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
const handleSlotChange = () => { const handleSlotChange = () => {
this.__cleanStore(); this.__cleanStore();
this.__setupStore(); this.__setupStore();
this.__updateSelected(false); this.__updateSelected(false);
}; };
if (tabSlot) {
tabSlot.addEventListener('slotchange', handleSlotChange); tabSlot.addEventListener('slotchange', handleSlotChange);
} }
}
}
__setupStore() { __setupStore() {
/** @type {StoreEntry[]} */
this.__store = []; this.__store = [];
const buttons = this.querySelectorAll('[slot="tab"]'); const buttons = /** @type {HTMLElement[]} */ (Array.from(
const panels = this.querySelectorAll('[slot="panel"]'); this.querySelectorAll('[slot="tab"]'),
));
const panels = /** @type {HTMLElement[]} */ (Array.from(
this.querySelectorAll('[slot="panel"]'),
));
if (buttons.length !== panels.length) { if (buttons.length !== panels.length) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(
@ -149,19 +200,25 @@ export class LionTabs extends LitElement {
buttons.forEach((button, index) => { buttons.forEach((button, index) => {
const uid = uuid(); const uid = uuid();
const panel = panels[index]; const panel = panels[index];
/** @type {StoreEntry} */
const entry = { const entry = {
uid, uid,
el: button,
button, button,
panel, panel,
clickHandler: this.__createButtonClickHandler(index), clickHandler: this.__createButtonClickHandler(index),
keydownHandler: handleButtonKeydown, keydownHandler: handleButtonKeydown.bind(this),
keyupHandler: this.__handleButtonKeyup.bind(this), keyupHandler: this.__handleButtonKeyup.bind(this),
}; };
setupPanel({ element: entry.panel, ...entry }); setupPanel({ ...entry, el: entry.panel });
setupButton({ element: entry.button, ...entry }); setupButton(entry);
deselectPanel(entry.panel); deselectPanel(entry.panel);
deselectButton(entry.button); deselectButton(entry.button);
if (this.__store) {
this.__store.push(entry); this.__store.push(entry);
}
}); });
} }
@ -170,18 +227,27 @@ export class LionTabs extends LitElement {
return; return;
} }
this.__store.forEach(entry => { this.__store.forEach(entry => {
cleanButton({ element: entry.button, ...entry }); cleanButton(entry);
}); });
} }
/**
* @param {number} index
* @returns {EventHandlerNonNull}
*/
__createButtonClickHandler(index) { __createButtonClickHandler(index) {
return () => { return () => {
this._setSelectedIndexWithFocus(index); this._setSelectedIndexWithFocus(index);
}; };
} }
__handleButtonKeyup(e) { /**
switch (e.key) { * @param {Event} ev
*/
__handleButtonKeyup(ev) {
const _ev = /** @type {KeyboardEvent} */ (ev);
if (typeof this.selectedIndex === 'number') {
switch (_ev.key) {
case 'ArrowDown': case 'ArrowDown':
case 'ArrowRight': case 'ArrowRight':
if (this.selectedIndex + 1 >= this._pairCount) { if (this.selectedIndex + 1 >= this._pairCount) {
@ -207,7 +273,11 @@ export class LionTabs extends LitElement {
/* no default */ /* no default */
} }
} }
}
/**
* @param {number} value The new index
*/
set selectedIndex(value) { set selectedIndex(value) {
const stale = this.__selectedIndex; const stale = this.__selectedIndex;
this.__selectedIndex = value; this.__selectedIndex = value;
@ -216,6 +286,9 @@ export class LionTabs extends LitElement {
this.requestUpdate('selectedIndex', stale); this.requestUpdate('selectedIndex', stale);
} }
/**
* @param {number} value The new index for focus
*/
_setSelectedIndexWithFocus(value) { _setSelectedIndexWithFocus(value) {
const stale = this.__selectedIndex; const stale = this.__selectedIndex;
this.__selectedIndex = value; this.__selectedIndex = value;
@ -224,24 +297,29 @@ export class LionTabs extends LitElement {
this.requestUpdate('selectedIndex', stale); this.requestUpdate('selectedIndex', stale);
} }
/**
* @return {number}
*/
get selectedIndex() { get selectedIndex() {
return this.__selectedIndex; return this.__selectedIndex || 0;
} }
get _pairCount() { get _pairCount() {
return this.__store.length; return (this.__store && this.__store.length) || 0;
} }
__updateSelected(withFocus = false) { __updateSelected(withFocus = false) {
if (!(this.__store && this.__store[this.selectedIndex])) { if (
!(this.__store && typeof this.selectedIndex === 'number' && this.__store[this.selectedIndex])
) {
return; return;
} }
const previousButton = Array.from(this.children).find( const previousButton = /** @type {HTMLElement} */ (Array.from(this.children).find(
child => child.slot === 'tab' && child.hasAttribute('selected'), child => child.slot === 'tab' && child.hasAttribute('selected'),
); ));
const previousPanel = Array.from(this.children).find( const previousPanel = /** @type {HTMLElement} */ (Array.from(this.children).find(
child => child.slot === 'panel' && child.hasAttribute('selected'), child => child.slot === 'panel' && child.hasAttribute('selected'),
); ));
if (previousButton) { if (previousButton) {
deselectButton(previousButton); deselectButton(previousButton);
} }

View file

@ -1,6 +1,9 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
/**
* @typedef {import('../src/LionTabs.js').LionTabs} LionTabs
*/
import '../lion-tabs.js'; import '../lion-tabs.js';
const basicTabs = html` const basicTabs = html`
@ -17,36 +20,34 @@ const basicTabs = html`
describe('<lion-tabs>', () => { describe('<lion-tabs>', () => {
describe('Tabs', () => { describe('Tabs', () => {
it('sets selectedIndex to 0 by default', async () => { it('sets selectedIndex to 0 by default', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
expect(el.selectedIndex).to.equal(0); expect(el.selectedIndex).to.equal(0);
}); });
it('can programmatically set selectedIndex', async () => { it('can programmatically set selectedIndex', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs .selectedIndex=${1}> <lion-tabs .selectedIndex=${1}>
<div slot="tab">tab 1</div> <div slot="tab">tab 1</div>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<div slot="tab">tab 2</div> <div slot="tab">tab 2</div>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
expect(el.selectedIndex).to.equal(1); expect(el.selectedIndex).to.equal(1);
expect( let selectedTab = /** @type {Element} */ (Array.from(el.children).find(
Array.from(el.children).find(
child => child.slot === 'tab' && child.hasAttribute('selected'), child => child.slot === 'tab' && child.hasAttribute('selected'),
).textContent, ));
).to.equal('tab 2'); expect(selectedTab.textContent).to.equal('tab 2');
el.selectedIndex = 0; el.selectedIndex = 0;
expect( selectedTab = /** @type {Element} */ (Array.from(el.children).find(
Array.from(el.children).find(
child => child.slot === 'tab' && child.hasAttribute('selected'), child => child.slot === 'tab' && child.hasAttribute('selected'),
).textContent, ));
).to.equal('tab 1'); expect(selectedTab.textContent).to.equal('tab 1');
}); });
it('has [selected] on current selected tab which serves as styling hook', async () => { it('has [selected] on current selected tab which serves as styling hook', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
el.selectedIndex = 0; el.selectedIndex = 0;
expect(tabs[0]).to.have.attribute('selected'); expect(tabs[0]).to.have.attribute('selected');
@ -58,7 +59,7 @@ describe('<lion-tabs>', () => {
}); });
it('sends event "selected-changed" for every selected state change', async () => { it('sends event "selected-changed" for every selected state change', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const spy = sinon.spy(); const spy = sinon.spy();
el.addEventListener('selected-changed', spy); el.addEventListener('selected-changed', spy);
el.selectedIndex = 1; el.selectedIndex = 1;
@ -75,18 +76,18 @@ describe('<lion-tabs>', () => {
</lion-tabs> </lion-tabs>
`); `);
expect(spy.callCount).to.equal(1); expect(spy.callCount).to.equal(1);
console.warn.restore(); spy.restore();
}); });
}); });
describe('Tabs ([slot=tab])', () => { describe('Tabs ([slot=tab])', () => {
it('adds role=tab', async () => { it('adds role=tab', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab</button> <button slot="tab">tab</button>
<div slot="panel">panel</div> <div slot="panel">panel</div>
</lion-tabs> </lion-tabs>
`); `));
expect(Array.from(el.children).find(child => child.slot === 'tab')).to.have.attribute( expect(Array.from(el.children).find(child => child.slot === 'tab')).to.have.attribute(
'role', 'role',
'tab', 'tab',
@ -101,7 +102,7 @@ describe('<lion-tabs>', () => {
describe('Tab Panels (slot=panel)', () => { describe('Tab Panels (slot=panel)', () => {
it('are visible when corresponding tab is selected ', async () => { it('are visible when corresponding tab is selected ', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const panels = el.querySelectorAll('[slot=panel]'); const panels = el.querySelectorAll('[slot=panel]');
el.selectedIndex = 0; el.selectedIndex = 0;
expect(panels[0]).to.be.visible; expect(panels[0]).to.be.visible;
@ -125,14 +126,14 @@ describe('<lion-tabs>', () => {
*/ */
describe('User interaction', () => { describe('User interaction', () => {
it('selects a tab on click', async () => { it('selects a tab on click', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
tabs[1].dispatchEvent(new Event('click')); tabs[1].dispatchEvent(new Event('click'));
expect(el.selectedIndex).to.equal(1); expect(el.selectedIndex).to.equal(1);
}); });
it('selects next tab on [arrow-right] and [arrow-down]', async () => { it('selects next tab on [arrow-right] and [arrow-down]', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
expect(el.selectedIndex).to.equal(1); expect(el.selectedIndex).to.equal(1);
@ -141,7 +142,7 @@ describe('<lion-tabs>', () => {
}); });
it('selects previous tab on [arrow-left] and [arrow-up]', async () => { it('selects previous tab on [arrow-left] and [arrow-up]', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs .selectedIndex=${1}> <lion-tabs .selectedIndex=${1}>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
@ -150,7 +151,7 @@ describe('<lion-tabs>', () => {
<button slot="tab">tab 3</button> <button slot="tab">tab 3</button>
<div slot="panel">panel 3</div> <div slot="panel">panel 3</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
el.selectedIndex = 2; el.selectedIndex = 2;
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' })); tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
@ -160,29 +161,29 @@ describe('<lion-tabs>', () => {
}); });
it('selects first tab on [home]', async () => { it('selects first tab on [home]', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs .selectedIndex=${1}> <lion-tabs .selectedIndex=${1}>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' })); tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
expect(el.selectedIndex).to.equal(0); expect(el.selectedIndex).to.equal(0);
}); });
it('selects last tab on [end]', async () => { it('selects last tab on [end]', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' })); tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
expect(el.selectedIndex).to.equal(2); expect(el.selectedIndex).to.equal(2);
}); });
it('selects first tab on [arrow-right] if on last tab', async () => { it('selects first tab on [arrow-right] if on last tab', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs selectedIndex="2"> <lion-tabs .selectedIndex=${2}>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
@ -190,14 +191,14 @@ describe('<lion-tabs>', () => {
<button slot="tab">tab 3</button> <button slot="tab">tab 3</button>
<div slot="panel">panel 3</div> <div slot="panel">panel 3</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
expect(el.selectedIndex).to.equal(0); expect(el.selectedIndex).to.equal(0);
}); });
it('selects last tab on [arrow-left] if on first tab', async () => { it('selects last tab on [arrow-left] if on first tab', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
@ -206,7 +207,7 @@ describe('<lion-tabs>', () => {
<button slot="tab">tab 3</button> <button slot="tab">tab 3</button>
<div slot="panel">panel 3</div> <div slot="panel">panel 3</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' })); tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
expect(el.selectedIndex).to.equal(2); expect(el.selectedIndex).to.equal(2);
@ -215,7 +216,7 @@ describe('<lion-tabs>', () => {
describe('Content distribution', () => { describe('Content distribution', () => {
it('should work with append children', async () => { it('should work with append children', async () => {
const el = await fixture(basicTabs); const el = /** @type {LionTabs} */ (await fixture(basicTabs));
const c = 2; const c = 2;
const n = el.children.length / 2; const n = el.children.length / 2;
for (let i = n + 1; i < n + c + 1; i += 1) { for (let i = n + 1; i < n + c + 1; i += 1) {
@ -230,29 +231,28 @@ describe('<lion-tabs>', () => {
} }
el.selectedIndex = el.children.length / 2 - 1; el.selectedIndex = el.children.length / 2 - 1;
await el.updateComplete; await el.updateComplete;
expect( const selectedTab = Array.from(el.children).find(
Array.from(el.children).find(
child => child.slot === 'tab' && child.hasAttribute('selected'), child => child.slot === 'tab' && child.hasAttribute('selected'),
).textContent, );
).to.equal('tab 5'); expect(selectedTab && selectedTab.textContent).to.equal('tab 5');
expect(
Array.from(el.children).find( const selectedPanel = Array.from(el.children).find(
child => child.slot === 'panel' && child.hasAttribute('selected'), child => child.slot === 'panel' && child.hasAttribute('selected'),
).textContent, );
).to.equal('panel 5'); expect(selectedPanel && selectedPanel.textContent).to.equal('panel 5');
}); });
}); });
describe('Initializing without Focus', () => { describe('Initializing without Focus', () => {
it('does not focus a tab when setting selectedIndex property', async () => { it('does not focus a tab when setting selectedIndex property', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
el.selectedIndex = 1; el.selectedIndex = 1;
expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be
@ -260,56 +260,56 @@ describe('<lion-tabs>', () => {
}); });
it('does not focus a tab on firstUpdate', async () => { it('does not focus a tab on firstUpdate', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = Array.from(el.children).filter(child => child.slot === 'tab'); const tabs = Array.from(el.children).filter(child => child.slot === 'tab');
expect(tabs.some(tab => tab === document.activeElement)).to.be.false; expect(tabs.some(tab => tab === document.activeElement)).to.be.false;
}); });
it('focuses on a tab when setting with _setSelectedIndexWithFocus method', async () => { it('focuses on a tab when setting with _setSelectedIndexWithFocus method', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
el._setSelectedIndexWithFocus(1); el._setSelectedIndexWithFocus(1);
expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be.true; expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be.true;
}); });
}); });
it('focuses on a tab when the selected tab is changed by user interaction', async () => { it('focuses on a tab when the selected tab is changed by user interaction', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
const secondTab = el.querySelector('[slot="tab"]:nth-of-type(2)'); const secondTab = /** @type {Element} */ (el.querySelector('[slot="tab"]:nth-of-type(2)'));
secondTab.dispatchEvent(new MouseEvent('click')); secondTab.dispatchEvent(new MouseEvent('click'));
expect(secondTab === document.activeElement).to.be.true; expect(secondTab === document.activeElement).to.be.true;
}); });
describe('Accessibility', () => { describe('Accessibility', () => {
it('does not make panels focusable', async () => { it('does not make panels focusable', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
expect(Array.from(el.children).find(child => child.slot === 'panel')).to.not.have.attribute( expect(Array.from(el.children).find(child => child.slot === 'panel')).to.not.have.attribute(
'tabindex', 'tabindex',
); );
@ -319,7 +319,7 @@ describe('<lion-tabs>', () => {
}); });
it('makes selected tab focusable (other tabs are unfocusable)', async () => { it('makes selected tab focusable (other tabs are unfocusable)', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
@ -328,7 +328,7 @@ describe('<lion-tabs>', () => {
<button slot="tab">tab 3</button> <button slot="tab">tab 3</button>
<div slot="panel">panel 3</div> <div slot="panel">panel 3</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
expect(tabs[0]).to.have.attribute('tabindex', '0'); expect(tabs[0]).to.have.attribute('tabindex', '0');
expect(tabs[1]).to.have.attribute('tabindex', '-1'); expect(tabs[1]).to.have.attribute('tabindex', '-1');
@ -337,14 +337,14 @@ describe('<lion-tabs>', () => {
describe('Tabs', () => { describe('Tabs', () => {
it('links ids of content items to tab via [aria-controls]', async () => { it('links ids of content items to tab via [aria-controls]', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button id="t1" slot="tab">tab 1</button> <button id="t1" slot="tab">tab 1</button>
<div id="p1" slot="panel">panel 1</div> <div id="p1" slot="panel">panel 1</div>
<button id="t2" slot="tab">tab 2</button> <button id="t2" slot="tab">tab 2</button>
<div id="p2" slot="panel">panel 2</div> <div id="p2" slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
const panels = el.querySelectorAll('[slot=panel]'); const panels = el.querySelectorAll('[slot=panel]');
expect(tabs[0].getAttribute('aria-controls')).to.equal(panels[0].id); expect(tabs[0].getAttribute('aria-controls')).to.equal(panels[0].id);
@ -352,7 +352,7 @@ describe('<lion-tabs>', () => {
}); });
it('adds aria-selected=“true” to selected tab', async () => { it('adds aria-selected=“true” to selected tab', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button id="t1" slot="tab">tab 1</button> <button id="t1" slot="tab">tab 1</button>
<div id="p1" slot="panel">panel 1</div> <div id="p1" slot="panel">panel 1</div>
@ -361,7 +361,7 @@ describe('<lion-tabs>', () => {
<button id="t3" slot="tab">tab 3</button> <button id="t3" slot="tab">tab 3</button>
<div id="p3" slot="panel">panel 3</div> <div id="p3" slot="panel">panel 3</div>
</lion-tabs> </lion-tabs>
`); `));
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
expect(tabs[0].getAttribute('aria-selected')).to.equal('true'); expect(tabs[0].getAttribute('aria-selected')).to.equal('true');
@ -372,28 +372,28 @@ describe('<lion-tabs>', () => {
describe('panels', () => { describe('panels', () => {
it('adds role="tabpanel" to panels', async () => { it('adds role="tabpanel" to panels', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
const panels = el.querySelectorAll('[slot=panel]'); const panels = el.querySelectorAll('[slot=panel]');
expect(panels[0]).to.have.attribute('role', 'tabpanel'); expect(panels[0]).to.have.attribute('role', 'tabpanel');
expect(panels[1]).to.have.attribute('role', 'tabpanel'); expect(panels[1]).to.have.attribute('role', 'tabpanel');
}); });
it('adds aria-labelledby referring to tab ids', async () => { it('adds aria-labelledby referring to tab ids', async () => {
const el = await fixture(html` const el = /** @type {LionTabs} */ (await fixture(html`
<lion-tabs> <lion-tabs>
<button slot="tab">tab 1</button> <button slot="tab">tab 1</button>
<div slot="panel">panel 1</div> <div slot="panel">panel 1</div>
<button slot="tab">tab 2</button> <button slot="tab">tab 2</button>
<div slot="panel">panel 2</div> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`); `));
const panels = el.querySelectorAll('[slot=panel]'); const panels = el.querySelectorAll('[slot=panel]');
const tabs = el.querySelectorAll('[slot=tab]'); const tabs = el.querySelectorAll('[slot=tab]');
expect(panels[0]).to.have.attribute('aria-labelledby', tabs[0].id); expect(panels[0]).to.have.attribute('aria-labelledby', tabs[0].id);