Skip to content

Commit

Permalink
fix(sbb-tab-group): correctly select a new tab if it is 'active' (#3251)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomMenga authored and github-actions committed Dec 11, 2024
1 parent 61cf3bc commit c2f0f36
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 102 deletions.
256 changes: 156 additions & 100 deletions src/elements/tabs/tab-group/tab-group.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,136 +15,138 @@ import '../tab.js';
describe(`sbb-tab-group`, () => {
let element: SbbTabGroupElement;

beforeEach(async () => {
element = await fixture(
html`<sbb-tab-group initial-selected-index="1">
<sbb-tab-label id="sbb-tab-1">Test tab label 1</sbb-tab-label>
<sbb-tab>Test tab content 1</sbb-tab>
<sbb-tab-label id="sbb-tab-2">Test tab label 2</sbb-tab-label>
<sbb-tab>Test tab content 2</sbb-tab>
<sbb-tab-label id="sbb-tab-3" disabled>Test tab label 3</sbb-tab-label>
<sbb-tab>Test tab content 3</sbb-tab>
<sbb-tab-label id="sbb-tab-4">Test tab label 4</sbb-tab-label>
<sbb-tab>Test tab content 4</sbb-tab>
</sbb-tab-group>`,
);
});
describe('basic', () => {
beforeEach(async () => {
element = await fixture(
html`<sbb-tab-group initial-selected-index="1">
<sbb-tab-label id="sbb-tab-1">Test tab label 1</sbb-tab-label>
<sbb-tab>Test tab content 1</sbb-tab>
<sbb-tab-label id="sbb-tab-2">Test tab label 2</sbb-tab-label>
<sbb-tab>Test tab content 2</sbb-tab>
<sbb-tab-label id="sbb-tab-3" disabled>Test tab label 3</sbb-tab-label>
<sbb-tab>Test tab content 3</sbb-tab>
<sbb-tab-label id="sbb-tab-4">Test tab label 4</sbb-tab-label>
<sbb-tab>Test tab content 4</sbb-tab>
</sbb-tab-group>`,
);
});

it('renders', () => {
assert.instanceOf(element, SbbTabGroupElement);
});
it('renders', () => {
assert.instanceOf(element, SbbTabGroupElement);
});

it('renders tab content', async () => {
const content = element.querySelector<SbbTabElement>(
':scope > sbb-tab-label:first-of-type + sbb-tab',
)!;
it('renders tab content', async () => {
const content = element.querySelector<SbbTabElement>(
':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<SbbTabLabelElement>(':scope > sbb-tab-label#sbb-tab-1')!;
describe('events', () => {
it('selects tab on click', async () => {
const tab = element.querySelector<SbbTabLabelElement>(':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<SbbTabLabelElement>(':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<SbbTabLabelElement>(':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');
});
});
});

Expand Down Expand Up @@ -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`<sbb-tab-group></sbb-tab-group>`);
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');
});
});
11 changes: 9 additions & 2 deletions src/elements/tabs/tab-group/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
};

Expand Down Expand Up @@ -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`;
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -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();
});
Expand Down

0 comments on commit c2f0f36

Please sign in to comment.