Skip to content

Commit

Permalink
fix: observe tabs ID attribute changes and update panels (#7201)
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan committed Mar 18, 2024
1 parent 89f71cc commit 2f27b94
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 13 deletions.
51 changes: 38 additions & 13 deletions packages/tabsheet/src/vaadin-tabsheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,29 @@ class TabsSlotController extends SlotController {
super(host, 'tabs');
this.__tabsItemsChangedListener = this.__tabsItemsChangedListener.bind(this);
this.__tabsSelectedChangedListener = this.__tabsSelectedChangedListener.bind(this);
this.__tabIdObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
const tab = mutation.target;

host.__linkTabAndPanel(tab);

if (tab.selected) {
host.__togglePanels(tab);
}
});
});
}

/** @private */
__tabsItemsChangedListener() {
this.host._setItems(this.tabs.items);
this.__tabIdObserver.disconnect();
const items = this.tabs.items || [];
items.forEach((tab) => {
this.__tabIdObserver.observe(tab, {
attributeFilter: ['id'],
});
});
this.host._setItems(items);
}

/** @private */
Expand Down Expand Up @@ -242,16 +260,7 @@ class TabSheet extends ControllerMixin(DelegateStateMixin(ElementMixin(ThemableM
}

items.forEach((tabItem) => {
const panel = panels.find((panel) => panel.getAttribute('tab') === tabItem.id);
if (panel) {
panel.role = 'tabpanel';
if (!panel.id) {
panel.id = `tabsheet-panel-${generateUniqueId()}`;
}
panel.setAttribute('aria-labelledby', tabItem.id);

tabItem.setAttribute('aria-controls', panel.id);
}
this.__linkTabAndPanel(tabItem, panels);
});
}

Expand All @@ -264,11 +273,14 @@ class TabSheet extends ControllerMixin(DelegateStateMixin(ElementMixin(ThemableM
return;
}

const content = this.shadowRoot.querySelector('[part="content"]');
this.__togglePanels(items[selected], panels);
}

const selectedTab = items[selected];
/** @private */
__togglePanels(selectedTab, panels = this.__panels) {
const selectedTabId = selectedTab ? selectedTab.id : '';
const selectedPanel = panels.find((panel) => panel.getAttribute('tab') === selectedTabId);
const content = this.shadowRoot.querySelector('[part="content"]');

// Mark loading state if a selected panel is not found.
this.toggleAttribute('loading', !selectedPanel);
Expand All @@ -288,6 +300,19 @@ class TabSheet extends ControllerMixin(DelegateStateMixin(ElementMixin(ThemableM
panel.hidden = panel !== selectedPanel;
});
}

/** @private */
__linkTabAndPanel(tab, panels = this.__panels) {
const panel = panels.find((panel) => panel.getAttribute('tab') === tab.id);
if (panel) {
panel.role = 'tabpanel';
if (!panel.id) {
panel.id = `tabsheet-panel-${generateUniqueId()}`;
}
panel.setAttribute('aria-labelledby', tab.id);
tab.setAttribute('aria-controls', panel.id);
}
}
}

defineCustomElement(TabSheet);
Expand Down
86 changes: 86 additions & 0 deletions packages/tabsheet/test/tabsheet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,89 @@ describe('tabsheet - lazy tabs', () => {
expect(getPanels()[2].hidden).to.be.true;
});
});

describe('tabsheet - tabs without ID', () => {
let tabsheet, tabs;

function getPanels() {
return [...tabsheet.querySelectorAll(`[tab]`)];
}

beforeEach(async () => {
tabsheet = fixtureSync(`
<vaadin-tabsheet>
<vaadin-tabs slot="tabs">
<vaadin-tab>Tab 1</vaadin-tab>
<vaadin-tab>Tab 2</vaadin-tab>
</vaadin-tabs>
<div tab="tab-1">Panel 1</div>
<div tab="tab-2">Panel 2</div>
</vaadin-tabsheet>
`);

await nextFrame();
tabs = tabsheet.querySelector('vaadin-tabs');
});

it('should be in loading state until ID is set on the tab', () => {
expect(tabsheet.hasAttribute('loading')).to.be.true;
});

it('should not be in loading state after setting ID on the tab', async () => {
tabs.items[0].id = 'tab-1';
await nextFrame();
expect(tabsheet.hasAttribute('loading')).to.be.false;
});

it('should restore loading state after removing ID from the tab', async () => {
tabs.items[0].id = 'tab-1';
await nextFrame();

tabs.items[0].id = null;
await nextFrame();
expect(tabsheet.hasAttribute('loading')).to.be.true;
});

it('should have all panels hidden until ID is set on the tab', () => {
expect(getPanels()[0].hidden).to.be.true;
expect(getPanels()[1].hidden).to.be.true;
});

it('should have matching panel visible after setting ID on the selected tab', async () => {
tabs.items[0].id = 'tab-1';
await nextFrame();
expect(getPanels()[0].hidden).to.be.false;
expect(getPanels()[1].hidden).to.be.true;
});

it('should not have matching panel visible after setting ID on the not selected tab', async () => {
tabs.items[1].id = 'tab-2';
await nextFrame();
expect(getPanels()[0].hidden).to.be.true;
expect(getPanels()[1].hidden).to.be.true;
});

it('should not have matching panel visible after setting ID on the detached tab', async () => {
// Move selected tab out of the `vaadin-tabs`
const tab = tabs.items[0];
tabsheet.parentNode.appendChild(tab);
await nextFrame();
tab.id = 'tab-1';
await nextFrame();
expect(getPanels()[0].hidden).to.be.true;
expect(getPanels()[1].hidden).to.be.true;
});

it('should link tab with panel after setting ID regardless of tab selected state', async () => {
tabs.items[0].id = 'tab-1';
await nextFrame();
expect(tabs.items[0].getAttribute('aria-controls')).to.equal(getPanels()[0].id);
expect(getPanels()[0].getAttribute('aria-labelledby')).to.equal('tab-1');

tabs.items[1].id = 'tab-2';
await nextFrame();
expect(tabs.items[1].getAttribute('aria-controls')).to.equal(getPanels()[1].id);
expect(getPanels()[1].getAttribute('aria-labelledby')).to.equal('tab-2');
});
});

0 comments on commit 2f27b94

Please sign in to comment.