From c2f0f362e757b88ef0fb84944aade98884682b66 Mon Sep 17 00:00:00 2001 From: Tommaso Menga Date: Wed, 11 Dec 2024 10:18:35 +0100 Subject: [PATCH] fix(sbb-tab-group): correctly select a new tab if it is 'active' (#3251) --- src/elements/tabs/tab-group/tab-group.spec.ts | 256 +++++++++++------- src/elements/tabs/tab-group/tab-group.ts | 11 +- 2 files changed, 165 insertions(+), 102 deletions(-) diff --git a/src/elements/tabs/tab-group/tab-group.spec.ts b/src/elements/tabs/tab-group/tab-group.spec.ts index deadfabdc5..3d2bf5d934 100644 --- a/src/elements/tabs/tab-group/tab-group.spec.ts +++ b/src/elements/tabs/tab-group/tab-group.spec.ts @@ -1,4 +1,4 @@ -import { assert, expect } from '@open-wc/testing'; +import { assert, aTimeout, expect } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; @@ -15,136 +15,138 @@ import '../tab.js'; describe(`sbb-tab-group`, () => { let element: SbbTabGroupElement; - beforeEach(async () => { - element = await fixture( - html` - Test tab label 1 - Test tab content 1 - Test tab label 2 - Test tab content 2 - Test tab label 3 - Test tab content 3 - Test tab label 4 - Test tab content 4 - `, - ); - }); + describe('basic', () => { + beforeEach(async () => { + element = await fixture( + html` + Test tab label 1 + Test tab content 1 + Test tab label 2 + Test tab content 2 + Test tab label 3 + Test tab content 3 + Test tab label 4 + Test tab content 4 + `, + ); + }); - it('renders', () => { - assert.instanceOf(element, SbbTabGroupElement); - }); + it('renders', () => { + assert.instanceOf(element, SbbTabGroupElement); + }); - it('renders tab content', async () => { - const content = element.querySelector( - ':scope > sbb-tab-label:first-of-type + sbb-tab', - )!; + it('renders tab content', async () => { + const content = element.querySelector( + ':scope > sbb-tab-label:first-of-type + sbb-tab', + )!; - expect(content.textContent).to.be.equal('Test tab content 1'); - }); + expect(content.textContent).to.be.equal('Test tab content 1'); + }); - it('renders initial selected index', async () => { - const initialSelectedTab = element.querySelector(':scope > sbb-tab-label#sbb-tab-2'); + it('renders initial selected index', async () => { + const initialSelectedTab = element.querySelector(':scope > sbb-tab-label#sbb-tab-2'); - expect(initialSelectedTab).to.have.attribute('active'); - }); + expect(initialSelectedTab).to.have.attribute('active'); + }); - it('activates tab by index', async () => { - element.activateTab(1); - await waitForLitRender(element); - const tab = element.querySelectorAll('sbb-tab-label')[1]; + it('activates tab by index', async () => { + element.activateTab(1); + await waitForLitRender(element); + const tab = element.querySelectorAll('sbb-tab-label')[1]; - expect(tab).to.have.attribute('active'); - }); + expect(tab).to.have.attribute('active'); + }); - it('disables tab by index', async () => { - element.disableTab(0); - await waitForLitRender(element); - const tab = element.querySelectorAll('sbb-tab-label')[0]; + it('disables tab by index', async () => { + element.disableTab(0); + await waitForLitRender(element); + const tab = element.querySelectorAll('sbb-tab-label')[0]; - expect(tab).to.have.attribute('disabled'); - }); + expect(tab).to.have.attribute('disabled'); + }); - it('enables tab by index', async () => { - element.enableTab(2); - await waitForLitRender(element); - const tab = element.querySelectorAll('sbb-tab-label')[2]; + it('enables tab by index', async () => { + element.enableTab(2); + await waitForLitRender(element); + const tab = element.querySelectorAll('sbb-tab-label')[2]; - expect(tab).not.to.have.attribute('disabled'); - }); + expect(tab).not.to.have.attribute('disabled'); + }); - it('does not activate a disabled tab', async () => { - const tab = element.querySelectorAll('sbb-tab-label')[2]; + it('does not activate a disabled tab', async () => { + const tab = element.querySelectorAll('sbb-tab-label')[2]; - tab.disabled = true; - element.activateTab(2); - await waitForLitRender(element); - expect(tab).not.to.have.attribute('active'); - }); + tab.disabled = true; + element.activateTab(2); + await waitForLitRender(element); + expect(tab).not.to.have.attribute('active'); + }); - it('activates the first tab', () => { - const tab = element.querySelectorAll('sbb-tab-label')[1]; + it('activates the first tab', () => { + const tab = element.querySelectorAll('sbb-tab-label')[1]; - expect(tab).to.have.attribute('active'); - }); + expect(tab).to.have.attribute('active'); + }); - describe('events', () => { - it('selects tab on click', async () => { - const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1')!; + describe('events', () => { + it('selects tab on click', async () => { + const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1')!; - tab.click(); - await waitForLitRender(element); + tab.click(); + await waitForLitRender(element); - expect(tab).to.have.attribute('active'); - }); + expect(tab).to.have.attribute('active'); + }); - it('dispatches event on tab change', async () => { - const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1')!; - const changeSpy = new EventSpy(SbbTabGroupElement.events.didChange); + it('dispatches event on tab change', async () => { + const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1')!; + const changeSpy = new EventSpy(SbbTabGroupElement.events.didChange); - tab.click(); - await changeSpy.calledOnce(); - expect(changeSpy.count).to.be.equal(1); - }); + tab.click(); + await changeSpy.calledOnce(); + expect(changeSpy.count).to.be.equal(1); + }); - it('selects tab on left arrow key pressed', async () => { - await sendKeys({ press: tabKey }); - await sendKeys({ press: 'ArrowLeft' }); - await waitForLitRender(element); - const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1'); + it('selects tab on left arrow key pressed', async () => { + await sendKeys({ press: tabKey }); + await sendKeys({ press: 'ArrowLeft' }); + await waitForLitRender(element); + const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1'); - expect(tab).to.have.attribute('active'); - }); + expect(tab).to.have.attribute('active'); + }); - it('selects tab on right arrow key pressed', async () => { - await sendKeys({ press: tabKey }); - await sendKeys({ press: 'ArrowRight' }); - await waitForLitRender(element); + it('selects tab on right arrow key pressed', async () => { + await sendKeys({ press: tabKey }); + await sendKeys({ press: 'ArrowRight' }); + await waitForLitRender(element); - const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-4'); + const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-4'); - expect(tab).to.have.attribute('active'); - }); + expect(tab).to.have.attribute('active'); + }); - it('wraps around on arrow key navigation', async () => { - await sendKeys({ press: tabKey }); - await sendKeys({ press: 'ArrowRight' }); - await sendKeys({ press: 'ArrowRight' }); - await waitForLitRender(element); + it('wraps around on arrow key navigation', async () => { + await sendKeys({ press: tabKey }); + await sendKeys({ press: 'ArrowRight' }); + await sendKeys({ press: 'ArrowRight' }); + await waitForLitRender(element); - const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1'); + const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-1'); - expect(tab).to.have.attribute('active'); - }); + expect(tab).to.have.attribute('active'); + }); - it('wraps around on arrow left arrow key navigation', async () => { - await sendKeys({ press: tabKey }); - await sendKeys({ press: 'ArrowLeft' }); - await sendKeys({ press: 'ArrowLeft' }); - await waitForLitRender(element); + it('wraps around on arrow left arrow key navigation', async () => { + await sendKeys({ press: tabKey }); + await sendKeys({ press: 'ArrowLeft' }); + await sendKeys({ press: 'ArrowLeft' }); + await waitForLitRender(element); - const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-4'); + const tab = element.querySelector(':scope > sbb-tab-label#sbb-tab-4'); - expect(tab).to.have.attribute('active'); + expect(tab).to.have.attribute('active'); + }); }); }); @@ -173,4 +175,58 @@ describe(`sbb-tab-group`, () => { const tab = element.querySelector('sbb-tab-label#sbb-tab-2'); expect(tab).to.have.attribute('active'); }); + + it('recovers if active tabs are added later', async () => { + element = await fixture(html``); + const changeSpy = new EventSpy(SbbTabGroupElement.events.didChange); + + const newLabel = document.createElement('sbb-tab-label'); + newLabel.textContent = 'Label 1'; + newLabel.toggleAttribute('active', true); + const newTab = document.createElement('sbb-tab'); + newTab.textContent = 'Tab 1'; + + element.append(newLabel, newTab); + + await waitForLitRender(element); + // Await throttling + await aTimeout(200); + + // console.log(element._selectedTab) + const newLabelActive = document.createElement('sbb-tab-label'); + newLabelActive.textContent = 'Label 2'; + const newTabActive = document.createElement('sbb-tab'); + newTabActive.textContent = 'Tab 2'; + + element.append(newLabelActive, newTabActive); + + await waitForLitRender(element); + // Await throttling + await aTimeout(200); + + expect(changeSpy.count).to.be.equal(1); + expect(element.querySelector('sbb-tab-label')).to.have.attribute('active'); + expect(element.querySelector('sbb-tab-label')).to.have.attribute('aria-selected', 'true'); + expect(element.querySelector('sbb-tab')).to.have.attribute('active'); + expect(element.querySelector('sbb-tab-label:nth-of-type(2)')).not.to.have.attribute('active'); + expect(element.querySelector('sbb-tab-label:nth-of-type(2)')).to.have.attribute( + 'aria-selected', + 'false', + ); + expect(element.querySelector('sbb-tab:nth-of-type(2)')).not.to.have.attribute('active'); + + newLabelActive.click(); + await waitForLitRender(element); + + expect(changeSpy.count).to.be.equal(2); + expect(element.querySelector('sbb-tab-label')).not.to.have.attribute('active'); + expect(element.querySelector('sbb-tab-label')).to.have.attribute('aria-selected', 'false'); + expect(element.querySelector('sbb-tab')).not.to.have.attribute('active'); + expect(element.querySelector('sbb-tab-label:nth-of-type(2)')).to.have.attribute('active'); + expect(element.querySelector('sbb-tab-label:nth-of-type(2)')).to.have.attribute( + 'aria-selected', + 'true', + ); + expect(element.querySelector('sbb-tab:nth-of-type(2)')).to.have.attribute('active'); + }); }); diff --git a/src/elements/tabs/tab-group/tab-group.ts b/src/elements/tabs/tab-group/tab-group.ts index 7cf1a3ca27..384dc2b0a7 100644 --- a/src/elements/tabs/tab-group/tab-group.ts +++ b/src/elements/tabs/tab-group/tab-group.ts @@ -171,6 +171,9 @@ class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { if (loadedTabs.length) { loadedTabs.forEach((tab) => this._configure(tab)); this._tabs = this._tabs.concat(loadedTabs); + + // If there is an active tab in the new batch, it becomes the new selected + loadedTabs.find((tab) => tab.active)?.tabGroupActions?.select(); } }; @@ -246,10 +249,13 @@ class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { } private _onTabContentElementResize(entries: ResizeObserverEntry[]): void { + if (!this._tabContentElement) { + return; + } for (const entry of entries) { const contentHeight = Math.floor(entry.contentRect.height); - (this._tabContentElement as HTMLElement).style.height = `${contentHeight}px`; + this._tabContentElement.style.height = `${contentHeight}px`; } } @@ -319,6 +325,7 @@ class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { } }, }; + if (tabLabel.nextElementSibling?.localName === 'sbb-tab') { tabLabel.tab = tabLabel.nextElementSibling as SbbTabElement; tabLabel.tab.id = this._assignId(); @@ -337,7 +344,7 @@ class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { tabLabel.disabled = tabLabel.hasAttribute('disabled'); tabLabel.active = tabLabel.hasAttribute('active') && !tabLabel.disabled; tabLabel.setAttribute('role', 'tab'); - tabLabel.setAttribute('aria-selected', 'false'); + tabLabel.setAttribute('aria-selected', String(tabLabel.active)); tabLabel.addEventListener('click', () => { tabLabel.tabGroupActions?.select(); });