import { expect, fixture } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import sinon from 'sinon'; /** * @typedef {import('../src/LionTabs.js').LionTabs} LionTabs */ import '@lion/tabs/define'; const basicTabs = html`
panel 1
panel 2
panel 3
panel 4
`; describe('', () => { describe('Tabs', () => { it('sets selectedIndex to 0 by default', async () => { const el = /** @type {LionTabs} */ (await fixture(basicTabs)); expect(el.selectedIndex).to.equal(0); }); it('can programmatically set selectedIndex', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
tab 1
panel 1
tab 2
panel 2
`) ); expect(el.selectedIndex).to.equal(1); let selectedTab = /** @type {Element} */ ( Array.from(el.children).find( child => child.slot === 'tab' && child.hasAttribute('selected'), ) ); expect(selectedTab.textContent).to.equal('tab 2'); el.selectedIndex = 0; selectedTab = /** @type {Element} */ ( Array.from(el.children).find( child => child.slot === 'tab' && child.hasAttribute('selected'), ) ); expect(selectedTab.textContent).to.equal('tab 1'); }); it('has [selected] on current selected tab which serves as styling hook', async () => { const el = /** @type {LionTabs} */ (await fixture(basicTabs)); const tabs = el.querySelectorAll('[slot=tab]'); el.selectedIndex = 0; expect(tabs[0]).to.have.attribute('selected'); expect(tabs[1]).to.not.have.attribute('selected'); el.selectedIndex = 1; expect(tabs[0]).to.not.have.attribute('selected'); expect(tabs[1]).to.have.attribute('selected'); }); it('sends event "selected-changed" for every selected state change', async () => { const el = /** @type {LionTabs} */ (await fixture(basicTabs)); const spy = sinon.spy(); el.addEventListener('selected-changed', spy); el.selectedIndex = 1; expect(spy).to.have.been.calledOnce; }); it('logs warning if unequal amount of tabs and panels', async () => { const stub = sinon.stub(console, 'warn'); await fixture(html`
panel 1
panel 2
`); expect(stub).to.be.calledOnceWithExactly( `The amount of tabs (1) doesn't match the amount of panels (2).`, ); stub.restore(); }); it('only takes direct children into account', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
nested panel
panel 2
`) ); el.selectedIndex = 1; const selectedTab = /** @type {Element} */ ( Array.from(el.children).find( child => child.slot === 'tab' && child.hasAttribute('selected'), ) ); expect(selectedTab.textContent).to.equal('tab 2'); }); }); describe('Tabs ([slot=tab])', () => { it('adds role=tab', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel
`) ); expect(Array.from(el.children).find(child => child.slot === 'tab')).to.have.attribute( 'role', 'tab', ); }); /** * Not in scope: * - has flexible html that allows animations like the material design underline */ }); describe('Tab Panels (slot=panel)', () => { it('are visible when corresponding tab is selected ', async () => { const el = /** @type {LionTabs} */ (await fixture(basicTabs)); const panels = el.querySelectorAll('[slot=panel]'); el.selectedIndex = 0; expect(panels[0]).to.be.visible; expect(panels[1]).to.be.not.visible; el.selectedIndex = 1; expect(panels[0]).to.be.not.visible; expect(panels[1]).to.be.visible; }); it.skip('have a DOM structure that allows them to be animated ', async () => {}); }); /** * We will immediately switch content as all our content comes from light dom. * * See Note at https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-19 * > It is recommended that tabs activate automatically when they receive focus as long as their * > associated tab panels are displayed without noticeable latency. This typically requires tab * > panel content to be preloaded. */ describe('User interaction', () => { it('selects a tab on click', async () => { 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 = /** @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); tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' })); expect(el.selectedIndex).to.equal(2); }); it('selects previous tab on [arrow-left] and [arrow-up]', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
`) ); const tabs = el.querySelectorAll('[slot=tab]'); el.selectedIndex = 2; tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' })); expect(el.selectedIndex).to.equal(1); tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp' })); expect(el.selectedIndex).to.equal(0); }); it('selects first tab on [home]', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); 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 = /** @type {LionTabs} */ (await fixture(basicTabs)); const tabs = el.querySelectorAll('[slot=tab]'); tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'End' })); expect(el.selectedIndex).to.equal(3); }); it('selects first tab on [arrow-right] if on last tab', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
`) ); 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 = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
`) ); const tabs = el.querySelectorAll('[slot=tab]'); tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' })); expect(el.selectedIndex).to.equal(2); }); it('selects next available not disabled tab if first tab is disabled', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
`) ); expect(el.selectedIndex).to.equal(1); }); it('selects next available not disabled tab on [arrow-right] and [arrow-down]', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
panel 3
`) ); const tabs = el.querySelectorAll('[slot=tab]'); tabs[0].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); expect(el.selectedIndex).to.equal(2); }); it('selects next available not disabled tab on [arrow-left] and [arrow-up]', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
panel 3
`) ); el.selectedIndex = 2; const tabs = el.querySelectorAll('[slot=tab]'); tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' })); expect(el.selectedIndex).to.equal(0); }); it('cycles through tabs even if the last or first tabs are disabled', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 0
panel 1
panel 2
panel 3
`) ); const tabs = el.querySelectorAll('[slot=tab]'); el.selectedIndex = 1; // 0 is disabled, same for 3, so should go to 2 tabs[1].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' })); expect(el.selectedIndex).to.equal(2); // 3 is disabled, same for 0, so should go to 1 tabs[2].dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' })); expect(el.selectedIndex).to.equal(1); }); }); describe('Content distribution', () => { it('should work with append children', async () => { 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) { const tab = document.createElement('button'); tab.setAttribute('slot', 'tab'); tab.innerText = `tab ${i}`; const panel = document.createElement('panel'); panel.setAttribute('slot', 'panel'); panel.innerText = `panel ${i}`; el.append(tab); el.append(panel); } el.selectedIndex = el.children.length / 2 - 1; await el.updateComplete; const selectedTab = Array.from(el.children).find( child => child.slot === 'tab' && child.hasAttribute('selected'), ); expect(selectedTab && selectedTab.textContent).to.equal('tab 6'); const selectedPanel = Array.from(el.children).find( child => child.slot === 'panel' && child.hasAttribute('selected'), ); expect(selectedPanel && selectedPanel.textContent).to.equal('panel 6'); }); }); describe('Initializing without Focus', () => { it('does not focus a tab when setting selectedIndex property', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); el.selectedIndex = 1; expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be .false; }); it('does not focus a tab on firstUpdate', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); 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 = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); // @ts-ignore : this el is LionTabs 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 = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 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 = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); expect(Array.from(el.children).find(child => child.slot === 'panel')).to.not.have.attribute( 'tabindex', ); expect(Array.from(el.children).find(child => child.slot === 'panel')).to.not.have.attribute( 'tabindex', ); }); it('makes selected tab focusable (other tabs are unfocusable)', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
`) ); const tabs = el.querySelectorAll('[slot=tab]'); expect(tabs[0]).to.have.attribute('tabindex', '0'); expect(tabs[1]).to.have.attribute('tabindex', '-1'); expect(tabs[2]).to.have.attribute('tabindex', '-1'); }); describe('Tabs', () => { it('links ids of content items to tab via [aria-controls]', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); const tabs = el.querySelectorAll('[slot=tab]'); const panels = el.querySelectorAll('[slot=panel]'); expect(tabs[0].getAttribute('aria-controls')).to.equal(panels[0].id); expect(tabs[1].getAttribute('aria-controls')).to.equal(panels[1].id); }); it('adds aria-selected=“true” to selected tab', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
panel 3
`) ); const tabs = el.querySelectorAll('[slot=tab]'); expect(tabs[0].getAttribute('aria-selected')).to.equal('true'); expect(tabs[1].getAttribute('aria-selected')).to.equal('false'); expect(tabs[2].getAttribute('aria-selected')).to.equal('false'); }); }); describe('panels', () => { it('adds role="tabpanel" to panels', async () => { const el = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); 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 = /** @type {LionTabs} */ ( await fixture(html`
panel 1
panel 2
`) ); const panels = el.querySelectorAll('[slot=panel]'); const tabs = el.querySelectorAll('[slot=tab]'); expect(panels[0]).to.have.attribute('aria-labelledby', tabs[0].id); expect(panels[1]).to.have.attribute('aria-labelledby', tabs[1].id); }); }); }); /** * Not in scope: * - allow to delete tabs * * For extending layer with Design system: * - add options for alignment (justify, right, left,center) of tabs * - add option for large tabs */ });