Skip to content

Commit 8d1c3b7

Browse files
authored
feat: auto associate tabpanels with tabs (#33939)
1 parent c28f731 commit 8d1c3b7

File tree

4 files changed

+87
-0
lines changed

4 files changed

+87
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "feat: auto associate tabpanels with tabs",
4+
"packageName": "@fluentui/web-components",
5+
"email": "machi@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/src/tablist/tablist.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,39 @@ test.describe('Tablist', () => {
310310

311311
await expect(element).toHaveJSProperty('activeid', secondTabId);
312312
});
313+
314+
test('should associate panel elements with `aria-controls` attributes', async ({ fastPage, page }) => {
315+
const { element } = fastPage;
316+
await fastPage.setTemplate(`
317+
<fluent-tablist>
318+
<fluent-tab aria-controls="panel1">Tab one</fluent-tab>
319+
<fluent-tab aria-controls="panel2">Tab two</fluent-tab>
320+
<fluent-tab aria-controls="panel3">Tab three</fluent-tab>
321+
</fluent-tablist>
322+
<div id="panel1">Panel one</div>
323+
<div id="panel2">Panel two</div>
324+
<div id="panel3">Panel three</div>
325+
`);
326+
327+
const tabs = element.getByRole('tab');
328+
const firstTab = tabs.nth(0);
329+
const secondTab = tabs.nth(1);
330+
const firstPanel = page.getByText('Panel one');
331+
const secondPanel = page.getByText('Panel two');
332+
const thirdPanel = page.getByText('Panel three');
333+
334+
await expect(firstTab).toHaveAttribute('aria-selected', 'true');
335+
await expect(firstPanel).toBeVisible();
336+
await expect(firstPanel).toHaveRole('tabpanel');
337+
await expect(secondPanel).toBeHidden();
338+
await expect(secondPanel).toHaveRole('tabpanel');
339+
await expect(thirdPanel).toBeHidden();
340+
await expect(thirdPanel).toHaveRole('tabpanel');
341+
342+
await secondTab.click();
343+
344+
await expect(firstPanel).toBeHidden();
345+
await expect(secondPanel).toBeVisible();
346+
await expect(thirdPanel).toBeHidden();
347+
});
313348
});

packages/web-components/src/tablist/tablist.stories.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,20 @@ export const LargeSizeVerticalOrientation: Story = {
148148
},
149149
],
150150
};
151+
152+
export const AutoPanelAssociation: Story = {
153+
render: renderComponent(html<StoryArgs<FluentTablist>>`
154+
<div style="display: flex; flex-direction: column; gap: 1rem;">
155+
<fluent-tablist>
156+
<fluent-tab aria-controls="panel1">First Tab</fluent-tab>
157+
<fluent-tab aria-controls="panel2">Second Tab</fluent-tab>
158+
<fluent-tab aria-controls="panel3">Third Tab</fluent-tab>
159+
<fluent-tab aria-controls="panel4">Fourth Tab</fluent-tab>
160+
</fluent-tablist>
161+
<div id="panel1">First panel</div>
162+
<div id="panel2">Second panel</div>
163+
<div id="panel3">Third panel</div>
164+
<div id="panel4">Fourth panel</div>
165+
</div>
166+
`),
167+
};

packages/web-components/src/tablist/tablist.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ export class BaseTablist extends FASTElement {
8585
if (this.$fastController.isConnected && this.tabs.length > 0) {
8686
this.prevActiveTabIndex = this.tabs.findIndex((item: HTMLElement) => item.id === oldValue);
8787
this.setTabs();
88+
89+
if (oldValue) {
90+
const prevActiveTab = this.tabs[this.prevActiveTabIndex];
91+
const prevActivePanel = this.tabPanelMap.get(prevActiveTab);
92+
if (prevActivePanel) {
93+
prevActivePanel.hidden = true;
94+
}
95+
}
96+
97+
if (newValue && this.activetab) {
98+
const activePanel = this.tabPanelMap.get(this.activetab);
99+
if (activePanel) {
100+
activePanel.hidden = false;
101+
}
102+
}
88103
}
89104
}
90105

@@ -100,6 +115,17 @@ export class BaseTablist extends FASTElement {
100115
if (this.$fastController.isConnected && this.tabs.length > 0) {
101116
this.tabIds = this.getTabIds();
102117
this.setTabs();
118+
119+
for (const tab of this.tabs) {
120+
const ariaControls = tab.getAttribute('aria-controls') ?? '';
121+
const rootNode = this.getRootNode() as Document | ShadowRoot;
122+
const panel = rootNode.getElementById(ariaControls);
123+
if (ariaControls && panel) {
124+
panel.role ??= 'tabpanel';
125+
panel.hidden = this.activeid !== tab.id;
126+
this.tabPanelMap.set(tab, panel);
127+
}
128+
}
103129
}
104130
}
105131

@@ -113,6 +139,8 @@ export class BaseTablist extends FASTElement {
113139
private activeTabIndex: number = 0;
114140
private tabIds!: Array<string>;
115141

142+
private tabPanelMap = new WeakMap<HTMLElement, HTMLElement>();
143+
116144
private change = (): void => {
117145
this.$emit('change', this.activetab);
118146
};

0 commit comments

Comments
 (0)