diff --git a/docs/components/tabs.md b/docs/components/tabs.md
index fe0af4604..5f331c0fb 100644
--- a/docs/components/tabs.md
+++ b/docs/components/tabs.md
@@ -231,7 +231,59 @@ content and establish hierarchy.
```
-
+## Accessibility
+
+Add an
+[`aria-label`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label)
+attribute to `` and any individual tab whose label needs to be more
+descriptive, such as icon-only tabs.
+
+```html
+
+
+ photo
+
+
+ videocam
+
+
+ audiotrack
+
+
+```
+
+### Tab panels
+
+Every tab must reference a
+[`role="tabpanel"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tabpanel_role)
+element with `aria-controls`. Tab panels must be labelled with `aria-label` or
+`aria-labelledby`.
+
+It's common to reference the panel's tab with `aria-labelledby`.
+
+```html
+
+
+ Photos
+
+
+ Videos
+
+
+ Music
+
+
+
+
+ ...
+
+
+ ...
+
+
+ ...
+
+```
## Theming
diff --git a/tabs/demo/stories.ts b/tabs/demo/stories.ts
index dfefaa1de..8ddd99bae 100644
--- a/tabs/demo/stories.ts
+++ b/tabs/demo/stories.ts
@@ -13,6 +13,7 @@ import '@material/web/tabs/secondary-tab.js';
import {MaterialStoryInit} from './material-collection.js';
import {MdTabs} from '@material/web/tabs/tabs.js';
import {css, html, nothing} from 'lit';
+import {ref} from 'lit/directives/ref.js';
/** Knob types for Tabs stories. */
export interface StoryKnobs {
@@ -23,15 +24,14 @@ export interface StoryKnobs {
}
const styles = css`
- .content:not([hidden]) {
- display: flex;
- align-items: center;
- padding: 16px;
- gap: 8px;
- min-block-size: 24px;
+ [role=tabpanel]:not([hidden]) {
font-family: Roboto, Material Sans, system-ui;
}
+ [role=tabpanel]:not(.subtabs) {
+ padding: 16px;
+ }
+
md-tabs {
--inline-size: 50vw;
min-inline-size: var(--inline-size);
@@ -44,7 +44,7 @@ const styles = css`
.controls {
height: 48px;
}
- `;
+`;
const primary: MaterialStoryInit = {
name: 'Primary Tabs',
@@ -54,26 +54,39 @@ const primary: MaterialStoryInit = {
const inlineIcon = knobs.inlineIcon;
return html`
-
-
+
${tabContent('piano', 'Keyboard')}
-
+
${tabContent('tune', 'Guitar')}
-
+
${tabContent('graphic_eq', 'Drums')}
-
+
${tabContent('speaker', 'Bass')}
-
+
${tabContent('nightlife', 'Saxophone')}
- `;
+
+
+
Keyboard
+ Guitar
+ Drums
+ Bass
+ Saxophone
+ `;
}
};
@@ -84,23 +97,30 @@ const secondary: MaterialStoryInit = {
const tabContent = getTabContentGenerator(knobs);
return html`
-
-
+
${tabContent('flight', 'Travel')}
-
+
${tabContent('hotel', 'Hotel')}
-
+
${tabContent('hiking', 'Activities')}
-
+
${tabContent('restaurant', 'Food')}
- `;
+
+
+ Travel
+ Hotel
+ Activities
+ Food
+ `;
}
};
@@ -112,7 +132,7 @@ const scrolling: MaterialStoryInit = {
const inlineIcon = knobs.inlineIcon;
return html`
- = {
],
render(knobs) {
const tabContent = getTabContentGenerator(knobs);
- const inlineIcon = knobs.inlineIcon;
return html`
-
-
+
${tabContent('flight', 'Travel')}
-
+
${tabContent('hotel', 'Hotel')}
-
+
${tabContent('hiking', 'Activities')}
-
+
${tabContent('restaurant', 'Food')}
- `;
+
+
+ Travel
+ Hotel
+ Activities
+ Food
+ `;
}
};
@@ -202,84 +227,77 @@ const primaryAndSecondary: MaterialStoryInit = {
const tabContent = getTabContentGenerator(knobs);
const inlineIcon = knobs.inlineIcon;
- // show the selected secondary tabs
- const handlePrimaryTabsChange = ({target}: Event) => {
- const primaryTabs = target as MdTabs;
- const secondaryTabsContainer = primaryTabs.nextElementSibling!;
- const secondaryTabsList = Array.from(
- secondaryTabsContainer.querySelectorAll('md-tabs'));
- secondaryTabsList.forEach((secondaryTabs, i) => {
- const hidden = i === primaryTabs.activeTabIndex ? false : true;
- secondaryTabs.hidden = hidden;
- (secondaryTabs.nextElementSibling! as HTMLElement).hidden = hidden;
- });
- };
-
- // renders some relevant tabbed content based on the current selection
- function handleSecondaryTabsChange({target}: Event) {
- const secondaryTabs = target as MdTabs;
- const contentContainer = secondaryTabs.nextElementSibling!;
- const content = Array.from(secondaryTabs.activeTab?.childNodes ?? [])
- .map(child => child.cloneNode(true));
- contentContainer.replaceChildren(...content);
- }
-
return html`
-
-
+
+ ${tabContent('videocam', 'Movies')}
+
+
+ ${tabContent('photo', 'Photos')}
+
+
+ ${tabContent('audiotrack', 'Music')}
+
+
+
+
+
-
- ${tabContent('videocam', 'Movies')}
-
-
- ${tabContent('photo', 'Photos')}
-
-
- ${tabContent('audiotrack', 'Music')}
-
+ Star Wars
+ Avengers
+ Jaws
+ Frozen
-
-
- Star Wars
- Avengers
- Jaws
- Frozen
-
-
-
- Yosemite
- Mona Lisa
- Swiss Alps
- Niagra Falls
-
-
-
- Rock
- Ambient
- Soundscapes
- White Noise
-
-
-
+
+
Star Wars
+
Avengers
+
Jaws
+
Frozen
- `;
+
+
+
+ Yosemite
+ Mona Lisa
+ Swiss Alps
+ Niagra Falls
+
+
+
Yosemite
+
Mona Lisa
+
Swiss Alps
+
Niagra Falls
+
+
+
+
+ Rock
+ Ambient
+ Soundscapes
+ White Noise
+
+
+
Rock
+
Ambient
+
Soundscapes
+
White Noise
+
+ `;
}
};
@@ -369,6 +387,29 @@ function getTabContentGenerator(knobs: StoryKnobs) {
};
}
+function setupTabPanels() {
+ return ref(instance => {
+ if (!instance) {
+ return;
+ }
+
+ const tabs = instance as MdTabs;
+ let currentPanel: HTMLElement|null = null;
+ tabs.addEventListener('change', () => {
+ if (currentPanel) {
+ currentPanel.hidden = true;
+ }
+
+ const panelId = tabs.activeTab?.getAttribute('aria-controls');
+ const root = tabs.getRootNode() as Document | ShadowRoot;
+ currentPanel = root.querySelector
(`#${panelId}`);
+ if (currentPanel) {
+ currentPanel.hidden = false;
+ }
+ });
+ });
+}
+
/** Tabs stories. */
export const stories =
[primary, secondary, scrolling, custom, primaryAndSecondary, dynamic];
diff --git a/tabs/internal/tab.ts b/tabs/internal/tab.ts
index 210c4ddb4..b7731dd3c 100644
--- a/tabs/internal/tab.ts
+++ b/tabs/internal/tab.ts
@@ -82,7 +82,7 @@ export class Tab extends LitElement {
protected override render() {
const indicator = html``;
return html`
-