feat(tabs): fix types and export type definition files for tabs
This commit is contained in:
parent
ec65da5da6
commit
0cf8a0c921
3 changed files with 238 additions and 157 deletions
|
|
@ -13,11 +13,14 @@
|
|||
"main": "index.js",
|
||||
"module": "index.js",
|
||||
"files": [
|
||||
"*.d.ts",
|
||||
"*.js",
|
||||
"docs",
|
||||
"src",
|
||||
"test",
|
||||
"translations"
|
||||
"test-helpers",
|
||||
"translations",
|
||||
"types"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
|
|
|
|||
|
|
@ -1,77 +1,114 @@
|
|||
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 }) => {
|
||||
element.setAttribute('id', `panel-${uid}`);
|
||||
element.setAttribute('role', 'tabpanel');
|
||||
element.setAttribute('aria-labelledby', `button-${uid}`);
|
||||
};
|
||||
|
||||
const selectPanel = element => {
|
||||
element.setAttribute('selected', true);
|
||||
};
|
||||
|
||||
const deselectPanel = element => {
|
||||
element.removeAttribute('selected');
|
||||
};
|
||||
|
||||
const setupButton = ({ element, uid, clickHandler, keydownHandler, keyupHandler }) => {
|
||||
element.setAttribute('id', `button-${uid}`);
|
||||
element.setAttribute('role', 'tab');
|
||||
element.setAttribute('aria-controls', `panel-${uid}`);
|
||||
element.addEventListener('click', clickHandler);
|
||||
element.addEventListener('keyup', keyupHandler);
|
||||
element.addEventListener('keydown', keydownHandler);
|
||||
};
|
||||
|
||||
const cleanButton = ({ element, clickHandler, keydownHandler, keyupHandler }) => {
|
||||
element.removeAttribute('id');
|
||||
element.removeAttribute('role');
|
||||
element.removeAttribute('aria-controls');
|
||||
element.removeEventListener('click', clickHandler);
|
||||
element.removeEventListener('keyup', keyupHandler);
|
||||
element.removeEventListener('keydown', keydownHandler);
|
||||
};
|
||||
|
||||
const selectButton = (element, withFocus = false) => {
|
||||
if (withFocus) {
|
||||
element.focus();
|
||||
function uuid() {
|
||||
return Math.random().toString(36).substr(2, 10);
|
||||
}
|
||||
|
||||
element.setAttribute('selected', true);
|
||||
element.setAttribute('aria-selected', true);
|
||||
element.setAttribute('tabindex', 0);
|
||||
};
|
||||
/**
|
||||
* @param {StoreEntry} options
|
||||
*/
|
||||
function setupPanel({ el, uid }) {
|
||||
el.setAttribute('id', `panel-${uid}`);
|
||||
el.setAttribute('role', 'tabpanel');
|
||||
el.setAttribute('aria-labelledby', `button-${uid}`);
|
||||
}
|
||||
|
||||
const deselectButton = element => {
|
||||
element.removeAttribute('selected');
|
||||
element.setAttribute('aria-selected', false);
|
||||
element.setAttribute('tabindex', -1);
|
||||
};
|
||||
/**
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
function selectPanel(el) {
|
||||
el.setAttribute('selected', 'true');
|
||||
}
|
||||
|
||||
const handleButtonKeydown = e => {
|
||||
switch (e.key) {
|
||||
/**
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
function deselectPanel(el) {
|
||||
el.removeAttribute('selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StoreEntry} options
|
||||
*/
|
||||
function setupButton({ el, uid, clickHandler, keydownHandler, keyupHandler }) {
|
||||
el.setAttribute('id', `button-${uid}`);
|
||||
el.setAttribute('role', 'tab');
|
||||
el.setAttribute('aria-controls', `panel-${uid}`);
|
||||
el.addEventListener('click', clickHandler);
|
||||
el.addEventListener('keyup', keyupHandler);
|
||||
el.addEventListener('keydown', keydownHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
el.focus();
|
||||
}
|
||||
|
||||
el.setAttribute('selected', 'true');
|
||||
el.setAttribute('aria-selected', 'true');
|
||||
el.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
function deselectButton(el) {
|
||||
el.removeAttribute('selected');
|
||||
el.setAttribute('aria-selected', 'false');
|
||||
el.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
*/
|
||||
function handleButtonKeydown(ev) {
|
||||
const _ev = /** @type {KeyboardEvent} */ (ev);
|
||||
switch (_ev.key) {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
case 'Home':
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
_ev.preventDefault();
|
||||
/* no default */
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class LionTabs extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* index number of the selected tab.
|
||||
*/
|
||||
selectedIndex: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
attribute: 'selected-index',
|
||||
reflect: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -117,28 +154,42 @@ export class LionTabs extends LitElement {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* An index number of the selected tab
|
||||
*/
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
super.firstUpdated();
|
||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__setupSlots();
|
||||
}
|
||||
|
||||
__setupSlots() {
|
||||
if (this.shadowRoot) {
|
||||
const tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
|
||||
const handleSlotChange = () => {
|
||||
this.__cleanStore();
|
||||
this.__setupStore();
|
||||
this.__updateSelected(false);
|
||||
};
|
||||
|
||||
if (tabSlot) {
|
||||
tabSlot.addEventListener('slotchange', handleSlotChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__setupStore() {
|
||||
/** @type {StoreEntry[]} */
|
||||
this.__store = [];
|
||||
const buttons = this.querySelectorAll('[slot="tab"]');
|
||||
const panels = this.querySelectorAll('[slot="panel"]');
|
||||
const buttons = /** @type {HTMLElement[]} */ (Array.from(
|
||||
this.querySelectorAll('[slot="tab"]'),
|
||||
));
|
||||
const panels = /** @type {HTMLElement[]} */ (Array.from(
|
||||
this.querySelectorAll('[slot="panel"]'),
|
||||
));
|
||||
if (buttons.length !== panels.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
|
|
@ -149,19 +200,25 @@ export class LionTabs extends LitElement {
|
|||
buttons.forEach((button, index) => {
|
||||
const uid = uuid();
|
||||
const panel = panels[index];
|
||||
|
||||
/** @type {StoreEntry} */
|
||||
const entry = {
|
||||
uid,
|
||||
el: button,
|
||||
button,
|
||||
panel,
|
||||
clickHandler: this.__createButtonClickHandler(index),
|
||||
keydownHandler: handleButtonKeydown,
|
||||
keydownHandler: handleButtonKeydown.bind(this),
|
||||
keyupHandler: this.__handleButtonKeyup.bind(this),
|
||||
};
|
||||
setupPanel({ element: entry.panel, ...entry });
|
||||
setupButton({ element: entry.button, ...entry });
|
||||
setupPanel({ ...entry, el: entry.panel });
|
||||
setupButton(entry);
|
||||
deselectPanel(entry.panel);
|
||||
deselectButton(entry.button);
|
||||
|
||||
if (this.__store) {
|
||||
this.__store.push(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -170,18 +227,27 @@ export class LionTabs extends LitElement {
|
|||
return;
|
||||
}
|
||||
this.__store.forEach(entry => {
|
||||
cleanButton({ element: entry.button, ...entry });
|
||||
cleanButton(entry);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @returns {EventHandlerNonNull}
|
||||
*/
|
||||
__createButtonClickHandler(index) {
|
||||
return () => {
|
||||
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 'ArrowRight':
|
||||
if (this.selectedIndex + 1 >= this._pairCount) {
|
||||
|
|
@ -207,7 +273,11 @@ export class LionTabs extends LitElement {
|
|||
/* no default */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value The new index
|
||||
*/
|
||||
set selectedIndex(value) {
|
||||
const stale = this.__selectedIndex;
|
||||
this.__selectedIndex = value;
|
||||
|
|
@ -216,6 +286,9 @@ export class LionTabs extends LitElement {
|
|||
this.requestUpdate('selectedIndex', stale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value The new index for focus
|
||||
*/
|
||||
_setSelectedIndexWithFocus(value) {
|
||||
const stale = this.__selectedIndex;
|
||||
this.__selectedIndex = value;
|
||||
|
|
@ -224,24 +297,29 @@ export class LionTabs extends LitElement {
|
|||
this.requestUpdate('selectedIndex', stale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
get selectedIndex() {
|
||||
return this.__selectedIndex;
|
||||
return this.__selectedIndex || 0;
|
||||
}
|
||||
|
||||
get _pairCount() {
|
||||
return this.__store.length;
|
||||
return (this.__store && this.__store.length) || 0;
|
||||
}
|
||||
|
||||
__updateSelected(withFocus = false) {
|
||||
if (!(this.__store && this.__store[this.selectedIndex])) {
|
||||
if (
|
||||
!(this.__store && typeof this.selectedIndex === 'number' && this.__store[this.selectedIndex])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const previousButton = Array.from(this.children).find(
|
||||
const previousButton = /** @type {HTMLElement} */ (Array.from(this.children).find(
|
||||
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'),
|
||||
);
|
||||
));
|
||||
if (previousButton) {
|
||||
deselectButton(previousButton);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
/**
|
||||
* @typedef {import('../src/LionTabs.js').LionTabs} LionTabs
|
||||
*/
|
||||
import '../lion-tabs.js';
|
||||
|
||||
const basicTabs = html`
|
||||
|
|
@ -17,36 +20,34 @@ const basicTabs = html`
|
|||
describe('<lion-tabs>', () => {
|
||||
describe('Tabs', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('can programmatically set selectedIndex', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs .selectedIndex=${1}>
|
||||
<div slot="tab">tab 1</div>
|
||||
<div slot="panel">panel 1</div>
|
||||
<div slot="tab">tab 2</div>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
expect(el.selectedIndex).to.equal(1);
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
let selectedTab = /** @type {Element} */ (Array.from(el.children).find(
|
||||
child => child.slot === 'tab' && child.hasAttribute('selected'),
|
||||
).textContent,
|
||||
).to.equal('tab 2');
|
||||
));
|
||||
expect(selectedTab.textContent).to.equal('tab 2');
|
||||
|
||||
el.selectedIndex = 0;
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
selectedTab = /** @type {Element} */ (Array.from(el.children).find(
|
||||
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 () => {
|
||||
const el = await fixture(basicTabs);
|
||||
const el = /** @type {LionTabs} */ (await fixture(basicTabs));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
el.selectedIndex = 0;
|
||||
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 () => {
|
||||
const el = await fixture(basicTabs);
|
||||
const el = /** @type {LionTabs} */ (await fixture(basicTabs));
|
||||
const spy = sinon.spy();
|
||||
el.addEventListener('selected-changed', spy);
|
||||
el.selectedIndex = 1;
|
||||
|
|
@ -75,18 +76,18 @@ describe('<lion-tabs>', () => {
|
|||
</lion-tabs>
|
||||
`);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
console.warn.restore();
|
||||
spy.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tabs ([slot=tab])', () => {
|
||||
it('adds role=tab', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab</button>
|
||||
<div slot="panel">panel</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
expect(Array.from(el.children).find(child => child.slot === 'tab')).to.have.attribute(
|
||||
'role',
|
||||
'tab',
|
||||
|
|
@ -101,7 +102,7 @@ describe('<lion-tabs>', () => {
|
|||
|
||||
describe('Tab Panels (slot=panel)', () => {
|
||||
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]');
|
||||
el.selectedIndex = 0;
|
||||
expect(panels[0]).to.be.visible;
|
||||
|
|
@ -125,14 +126,14 @@ describe('<lion-tabs>', () => {
|
|||
*/
|
||||
describe('User interaction', () => {
|
||||
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]');
|
||||
tabs[1].dispatchEvent(new Event('click'));
|
||||
expect(el.selectedIndex).to.equal(1);
|
||||
});
|
||||
|
||||
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]');
|
||||
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
|
||||
expect(el.selectedIndex).to.equal(1);
|
||||
|
|
@ -141,7 +142,7 @@ describe('<lion-tabs>', () => {
|
|||
});
|
||||
|
||||
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}>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
|
|
@ -150,7 +151,7 @@ describe('<lion-tabs>', () => {
|
|||
<button slot="tab">tab 3</button>
|
||||
<div slot="panel">panel 3</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
el.selectedIndex = 2;
|
||||
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||
|
|
@ -160,29 +161,29 @@ describe('<lion-tabs>', () => {
|
|||
});
|
||||
|
||||
it('selects first tab on [home]', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs .selectedIndex=${1}>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'Home' }));
|
||||
expect(el.selectedIndex).to.equal(0);
|
||||
});
|
||||
|
||||
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]');
|
||||
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' }));
|
||||
expect(el.selectedIndex).to.equal(2);
|
||||
});
|
||||
|
||||
it('selects first tab on [arrow-right] if on last tab', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-tabs selectedIndex="2">
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs .selectedIndex=${2}>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
|
|
@ -190,14 +191,14 @@ describe('<lion-tabs>', () => {
|
|||
<button slot="tab">tab 3</button>
|
||||
<div slot="panel">panel 3</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
|
||||
expect(el.selectedIndex).to.equal(0);
|
||||
});
|
||||
|
||||
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>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
|
|
@ -206,7 +207,7 @@ describe('<lion-tabs>', () => {
|
|||
<button slot="tab">tab 3</button>
|
||||
<div slot="panel">panel 3</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
|
||||
expect(el.selectedIndex).to.equal(2);
|
||||
|
|
@ -215,7 +216,7 @@ describe('<lion-tabs>', () => {
|
|||
|
||||
describe('Content distribution', () => {
|
||||
it('should work with append children', async () => {
|
||||
const el = await fixture(basicTabs);
|
||||
const el = /** @type {LionTabs} */ (await fixture(basicTabs));
|
||||
const c = 2;
|
||||
const n = el.children.length / 2;
|
||||
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;
|
||||
await el.updateComplete;
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
const selectedTab = Array.from(el.children).find(
|
||||
child => child.slot === 'tab' && child.hasAttribute('selected'),
|
||||
).textContent,
|
||||
).to.equal('tab 5');
|
||||
expect(
|
||||
Array.from(el.children).find(
|
||||
);
|
||||
expect(selectedTab && selectedTab.textContent).to.equal('tab 5');
|
||||
|
||||
const selectedPanel = Array.from(el.children).find(
|
||||
child => child.slot === 'panel' && child.hasAttribute('selected'),
|
||||
).textContent,
|
||||
).to.equal('panel 5');
|
||||
);
|
||||
expect(selectedPanel && selectedPanel.textContent).to.equal('panel 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initializing without Focus', () => {
|
||||
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>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
|
||||
el.selectedIndex = 1;
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = Array.from(el.children).filter(child => child.slot === 'tab');
|
||||
expect(tabs.some(tab => tab === document.activeElement)).to.be.false;
|
||||
});
|
||||
|
||||
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>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
el._setSelectedIndexWithFocus(1);
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</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'));
|
||||
expect(secondTab === document.activeElement).to.be.true;
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('does not make panels focusable', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
expect(Array.from(el.children).find(child => child.slot === 'panel')).to.not.have.attribute(
|
||||
'tabindex',
|
||||
);
|
||||
|
|
@ -319,7 +319,7 @@ describe('<lion-tabs>', () => {
|
|||
});
|
||||
|
||||
it('makes selected tab focusable (other tabs are unfocusable)', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
|
|
@ -328,7 +328,7 @@ describe('<lion-tabs>', () => {
|
|||
<button slot="tab">tab 3</button>
|
||||
<div slot="panel">panel 3</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
expect(tabs[0]).to.have.attribute('tabindex', '0');
|
||||
expect(tabs[1]).to.have.attribute('tabindex', '-1');
|
||||
|
|
@ -337,14 +337,14 @@ describe('<lion-tabs>', () => {
|
|||
|
||||
describe('Tabs', () => {
|
||||
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>
|
||||
<button id="t1" slot="tab">tab 1</button>
|
||||
<div id="p1" slot="panel">panel 1</div>
|
||||
<button id="t2" slot="tab">tab 2</button>
|
||||
<div id="p2" slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
const panels = el.querySelectorAll('[slot=panel]');
|
||||
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 () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button id="t1" slot="tab">tab 1</button>
|
||||
<div id="p1" slot="panel">panel 1</div>
|
||||
|
|
@ -361,7 +361,7 @@ describe('<lion-tabs>', () => {
|
|||
<button id="t3" slot="tab">tab 3</button>
|
||||
<div id="p3" slot="panel">panel 3</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
expect(tabs[0].getAttribute('aria-selected')).to.equal('true');
|
||||
|
|
@ -372,28 +372,28 @@ describe('<lion-tabs>', () => {
|
|||
|
||||
describe('panels', () => {
|
||||
it('adds role="tabpanel" to panels', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const panels = el.querySelectorAll('[slot=panel]');
|
||||
expect(panels[0]).to.have.attribute('role', 'tabpanel');
|
||||
expect(panels[1]).to.have.attribute('role', 'tabpanel');
|
||||
});
|
||||
|
||||
it('adds aria-labelledby referring to tab ids', async () => {
|
||||
const el = await fixture(html`
|
||||
const el = /** @type {LionTabs} */ (await fixture(html`
|
||||
<lion-tabs>
|
||||
<button slot="tab">tab 1</button>
|
||||
<div slot="panel">panel 1</div>
|
||||
<button slot="tab">tab 2</button>
|
||||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`);
|
||||
`));
|
||||
const panels = el.querySelectorAll('[slot=panel]');
|
||||
const tabs = el.querySelectorAll('[slot=tab]');
|
||||
expect(panels[0]).to.have.attribute('aria-labelledby', tabs[0].id);
|
||||
|
|
|
|||
Loading…
Reference in a new issue