From 58f24462467b549c810554d8bc78480673d36e3d Mon Sep 17 00:00:00 2001 From: Elizabeth Mitchell Date: Mon, 11 Sep 2023 11:42:36 -0700 Subject: [PATCH] fix(tabs): a11y and tabs sometimes not activating PiperOrigin-RevId: 564453926 --- docs/components/tabs.md | 54 ++++++++- tabs/demo/stories.ts | 241 +++++++++++++++++++++++----------------- tabs/internal/tab.ts | 9 +- 3 files changed, 202 insertions(+), 102 deletions(-) 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
+ + + + + `; } }; @@ -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
+ + + + `; } }; @@ -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
+ + + + `; } }; @@ -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 - -
- - - - -
+ +
Star Wars
+ + +
- `; + + + + + `; } }; @@ -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` -