You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(core): TabList reads its role and speaks the ARIA tabs pattern
The strip could only be a nav landmark with aria-current, so a consumer with real panels below it had to reimplement it to get role=tablist, aria-selected and aria-controls.
No new prop: TabList declares role?: AriaRole and reads it, the way LayoutHeader and LayoutPanel already declare theirs. role="tablist" asks for the tabs pattern, any other role passes through untouched, and left unset the strip picks from what it renders — tabs when it holds nothing but tabs, the nav landmark when it holds a menu, a link, or anything else. That changes the default for a strip of plain tabs.
The pick reads the rendered DOM rather than children, so a menu behind a conditional, inside a map, or wrapped in a consumer's own component counts, and it follows the strip if the contents change later. It settles because the markers it reads do not depend on the answer, and the wrapper stays a <nav> — role="none" takes the landmark away — so a change of pattern never remounts the tabs.
An href is only ignored where the role was asserted, and so is the warning for a tab that controls nothing; either panelId or a hand-written aria-controls satisfies it, and a hand-written one is never overwritten.
[breaking] TabList: the strip now speaks the WAI-ARIA tabs pattern — `role="tablist"` on the strip, `role="tab"` and `aria-selected` on the tabs, and `aria-controls` pointing at the panel each tab opens, from a new `panelId` prop on `Tab`. There is no new prop for it: `TabList` declares `role?: AriaRole` and reads it. `role="tablist"` asks for the pattern, any other role passes through to the element untouched, and **left unset the strip picks for itself** — the tabs pattern when it holds nothing but tabs, the navigation landmark when it holds anything else. The keyboard behaviour the pattern asks for was already there: arrows move between tabs, Tab leaves the strip.
6
+
7
+
**This changes the default.** A strip of plain tabs is a `<nav>` landmark today and becomes a tablist: the selected tab is announced as a selected tab rather than the current item, `aria-current` gives way to `aria-selected`, and — because a tablist reports itself as horizontal — ArrowUp and ArrowDown stop moving between tabs and scroll the page instead. ArrowLeft, ArrowRight, Home, End, Tab and the roving tab stop are unchanged, as is everything the strip looks like. A strip that really is page navigation keeps the landmark by saying so with `role="navigation"`.
8
+
9
+
Two things the strip will not do to you on its own. A tab with an `href` navigates, so a strip holding one stays navigation — an `href` is only ignored where `role="tablist"` was asked for explicitly, and then a development warning says so. And a tab that controls nothing is only worth mentioning to a consumer who asked for the pattern, so that warning too is limited to the explicit role; either `panelId` or an `aria-controls` you wrote yourself satisfies it, and a hand-written one is never overwritten. `aria-controls` is emitted only when you supply the id: pointing at a panel that does not exist is an invalid attribute value, which is worse than saying nothing.
10
+
11
+
The pick is made from the rendered DOM rather than from `children`, so a menu behind a conditional, inside a `.map`, or wrapped in a component of your own still counts, and it follows the strip if its contents change later — without remounting the tabs.
Copy file name to clipboardExpand all lines: packages/core/src/TabList/Tab.doc.mjs
+13-1Lines changed: 13 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -40,7 +40,13 @@ export const docs = {
40
40
name: 'href',
41
41
type: 'string',
42
42
description:
43
-
'URL to navigate to; when provided, the tab renders as an anchor element.',
43
+
'URL to navigate to; when provided, the tab renders as an anchor element and keeps the strip on the navigation pattern. Ignored in a TabList given an explicit role="tablist".',
44
+
},
45
+
{
46
+
name: 'panelId',
47
+
type: 'string',
48
+
description:
49
+
'Id of the panel this tab controls, wired up as aria-controls where the TabList speaks the tabs pattern. Put the same id on the panel element. No effect under the navigation pattern, including where the strip picked that pattern because it holds something that is not a tab.',
44
50
},
45
51
{
46
52
name: 'as',
@@ -138,6 +144,12 @@ export const docsZh = {
138
144
type: 'string',
139
145
description: '要导航到的 URL;提供时,标签渲染为锚点元素。',
140
146
},
147
+
{
148
+
name: 'panelId',
149
+
type: 'string',
150
+
description:
151
+
'Id of the panel this tab controls, wired up as aria-controls where the TabList speaks the tabs pattern. Put the same id on the panel element. No effect under the navigation pattern, including where the strip picked that pattern because it holds something that is not a tab.',
0 commit comments