Skip to content

Commit e2a7e4e

Browse files
RajdeepcRajdeep Chandraclaudepfulton
authored
fix(tabs): default keyboard-activation to automatic, matching React Spectrum (#6476)
<swc-tabs> inherited keyboard-activation="manual" as its default from 1st-gen sp-tabs' auto=false, not from a deliberate accessibility decision. React Spectrum/React Aria Tabs defaults to automatic activation, and swc-tab-panel content is always present in the light DOM (not lazily mounted), which is the condition the WAI-ARIA APG recommends for automatic activation. Flip the default to automatic and document manual as consumer guidance for expensive/lazy-loaded panels instead. Co-authored-by: Rajdeep Chandra <rajdeepc@adobe.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Patrick Fulton <360251+pfulton@users.noreply.github.com>
1 parent 37be44c commit e2a7e4e

9 files changed

Lines changed: 61 additions & 41 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@adobe/spectrum-wc': minor
3+
---
4+
5+
**fix(tabs):** Changed the default `keyboard-activation` on `<swc-tabs>` from `manual` to `automatic`, aligning with React Spectrum/React Aria `Tabs`.
6+
7+
`swc-tab-panel` content is always present in the light DOM (not lazily mounted), which is the condition the WAI-ARIA APG recommends for automatic activation. Consumers relying on the previous implicit `manual` default (inherited from 1st-gen `sp-tabs`' `auto = false`) should add `keyboard-activation="manual"` explicitly, particularly if their own panel content is expensive to render or lazy-loaded.

2nd-gen/packages/core/components/tabs/Tabs.base.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ export abstract class TabsBase extends SpectrumElement {
7171
static readonly VALID_DENSITIES: readonly TabDensity[] = TAB_DENSITIES;
7272

7373
/**
74-
* Whether selection follows keyboard focus (`automatic`) or the user
75-
* must press Enter or Space to activate (`manual`, default).
74+
* Whether selection follows keyboard focus (`automatic`, default) or the
75+
* user must press Enter or Space to activate (`manual`). Prefer `manual`
76+
* when tab panels are expensive to render or not fully present in the DOM.
7677
*
7778
* @see https://w3c.github.io/aria-practices/#kbd_selection_follows_focus
7879
*/

2nd-gen/packages/core/components/tabs/Tabs.types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ export const TABS_DEFAULT_DIRECTION =
2626
/**
2727
* How keyboard focus interacts with selection (Spectrum Design / S2 naming).
2828
*
29-
* - **`manual`**: Arrow keys move focus; Enter or Space activates (default).
30-
* - **`automatic`**: Selection follows focus as the user arrows between tabs.
29+
* - **`automatic`**: Selection follows focus as the user arrows between tabs
30+
* (default, matches React Spectrum/React Aria `Tabs`).
31+
* - **`manual`**: Arrow keys move focus; Enter or Space activates. Prefer this
32+
* for tab panels that are expensive to render or not fully present in the DOM.
3133
*/
3234
export const KEYBOARD_ACTIVATIONS = ['manual', 'automatic'] as const;
3335

3436
export type KeyboardActivation = (typeof KEYBOARD_ACTIVATIONS)[number];
3537

3638
export const KEYBOARD_ACTIVATION_DEFAULT =
37-
'manual' as const satisfies KeyboardActivation;
39+
'automatic' as const satisfies KeyboardActivation;
3840

3941
/**
4042
* Tab list density (Spectrum Design `density` naming).

2nd-gen/packages/swc/components/tabs/migration-guide.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ import '@adobe/spectrum-wc/components/tabs/swc-tab-panel.js';
4949

5050
| Addition | Notes |
5151
| ------------------------------- | ----------------------------------------------------------------- |
52-
| `keyboard-activation` attribute | `'manual'` (default) or `'automatic'` — replaces boolean `auto` |
52+
| `keyboard-activation` attribute | `'automatic'` (default) or `'manual'` — replaces boolean `auto` |
5353
| `density` attribute | `'regular'` (default) or `'compact'` — replaces boolean `compact` |
5454

5555
### Removed in Spectrum 2
5656

5757
| Removed | Replacement |
5858
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
59-
| `auto` boolean | `keyboard-activation="automatic"` |
59+
| `auto` boolean | `keyboard-activation` (see default change note below) |
6060
| `compact` boolean | `density="compact"` |
6161
| `quiet` boolean | No replacement; not part of Spectrum 2 surface |
6262
| `emphasized` boolean | No replacement; not part of Spectrum 2 surface |
@@ -67,6 +67,8 @@ import '@adobe/spectrum-wc/components/tabs/swc-tab-panel.js';
6767
| `rovingTabindexController` field | Removed; keyboard navigation is internal |
6868
| `focusElement` getter | Removed; use `tabsEl.focus()` |
6969

70+
> **Default activation mode changed.** In Spectrum 1, `<sp-tabs>` defaulted to manual activation (`auto` omitted or `false`): arrow keys moved focus without changing selection until Enter, Space, or click. Spectrum 2 defaults `keyboard-activation` to `automatic` to match React Spectrum's `Tabs`, so arrow keys now select immediately by default. If you were relying on the implicit manual default (no `auto` attribute) and your tab panels are expensive to render or not fully in the DOM, add `keyboard-activation="manual"` explicitly.
71+
7072
## Update your code
7173

7274
### 1. Rename the tags and attributes
@@ -208,7 +210,7 @@ Spectrum 2 uses a different custom property prefix. Spectrum 1 overrides will no
208210
- [ ] Replace `<sp-tabs>` with `<swc-tabs>`, `<sp-tab>` with `<swc-tab>`, `<sp-tab-panel>` with `<swc-tab-panel>`
209211
- [ ] Rename `value` attribute to `tab-id` on every tab and tab panel
210212
- [ ] Rename `label` attribute to `accessible-label` on `<swc-tabs>`
211-
- [ ] Replace boolean `auto` with `keyboard-activation="automatic"`
213+
- [ ] Replace boolean `auto` with `keyboard-activation`; if you relied on the implicit manual default in Spectrum 1, add `keyboard-activation="manual"` explicitly since Spectrum 2 defaults to `automatic`
212214
- [ ] Replace boolean `compact` with `density="compact"`
213215
- [ ] Remove `quiet`, `emphasized`, and `size` attributes
214216
- [ ] Replace `direction="vertical-right"` with `direction="vertical"`

2nd-gen/packages/swc/components/tabs/stories/tabs.stories.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const meta: Meta = {
6363
selected: '1',
6464
'accessible-label': 'Product details',
6565
direction: 'horizontal',
66-
'keyboard-activation': 'manual',
66+
'keyboard-activation': 'automatic',
6767
density: 'regular',
6868
disabled: false,
6969
},
@@ -75,7 +75,7 @@ export const meta: Meta = {
7575
accessibleLabel: (raw['accessible-label'] ?? 'Product details') as string,
7676
direction: args.direction as TabsDirection,
7777
keyboardActivation: (raw['keyboard-activation'] ??
78-
'manual') as KeyboardActivation,
78+
'automatic') as KeyboardActivation,
7979
density: args.density as TabDensity,
8080
disabled: Boolean(args.disabled),
8181
});
@@ -145,7 +145,7 @@ const renderTabGroup = ({
145145
direction = 'horizontal',
146146
accessibleLabel = 'Product details',
147147
selected = '1',
148-
keyboardActivation = 'manual',
148+
keyboardActivation = 'automatic',
149149
density = 'regular',
150150
disabled = false,
151151
tabs = defaultTabs,
@@ -316,25 +316,25 @@ export const States: Story = {
316316

317317
export const ActivationModes: Story = {
318318
render: () => html`
319-
<p><strong>Manual activation (default)</strong></p>
319+
<p><strong>Automatic activation (default)</strong></p>
320320
${renderTabGroup({
321-
accessibleLabel: 'Manual activation',
321+
accessibleLabel: 'Automatic activation',
322322
panels: html`
323323
<swc-tab-panel tab-id="1">
324-
<p>Use arrow keys to move focus, then Enter or Space to select.</p>
324+
<p>Arrow keys immediately select and display content.</p>
325325
</swc-tab-panel>
326326
<swc-tab-panel tab-id="2"><p>Specifications content.</p></swc-tab-panel>
327327
<swc-tab-panel tab-id="3"><p>Guidelines content.</p></swc-tab-panel>
328328
`,
329329
})}
330330
<br />
331-
<p><strong>Automatic activation</strong></p>
331+
<p><strong>Manual activation</strong></p>
332332
${renderTabGroup({
333-
keyboardActivation: 'automatic',
334-
accessibleLabel: 'Automatic activation',
333+
keyboardActivation: 'manual',
334+
accessibleLabel: 'Manual activation',
335335
panels: html`
336336
<swc-tab-panel tab-id="1">
337-
<p>Arrow keys immediately select and display content.</p>
337+
<p>Use arrow keys to move focus, then Enter or Space to select.</p>
338338
</swc-tab-panel>
339339
<swc-tab-panel tab-id="2"><p>Specifications content.</p></swc-tab-panel>
340340
<swc-tab-panel tab-id="3"><p>Guidelines content.</p></swc-tab-panel>

2nd-gen/packages/swc/components/tabs/tabs.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Properties that render visual content:
3737
- **accessible-label** (on `swc-tabs`): accessible name for the tablist
3838
- **aria-label** (on `swc-tab`): accessible name for icon-only tabs
3939
- **direction**: layout direction (`horizontal` or `vertical`)
40-
- **keyboard-activation**: `manual` (default) or `automatic` (selection follows focus)
40+
- **keyboard-activation**: `automatic` (default; selection follows focus) or `manual`
4141
- **density**: `regular` (default) or `compact` spacing
4242

4343
<Canvas of={TabsStories.Anatomy} />
@@ -80,9 +80,9 @@ Disabled tabs remain focusable via arrow keys but cannot be activated; see the [
8080

8181
### Activation Modes
8282

83-
`keyboard-activation="manual"` (default): arrow keys move focus between tabs without changing the selected tab until Enter or Space.
83+
`keyboard-activation="automatic"` (default): selection follows focus when arrowing, matching React Spectrum's `Tabs` behavior.
8484

85-
`keyboard-activation="automatic"`: selection follows focus when arrowing.
85+
`keyboard-activation="manual"`: arrow keys move focus between tabs without changing the selected tab until Enter or Space. Prefer this when tab panels are expensive to render or not fully present in the DOM.
8686

8787
#### Events
8888

@@ -114,7 +114,7 @@ The `<swc-tabs>` component implements several accessibility features:
114114
- <kbd>Home</kbd>: moves focus to the first tab
115115
- <kbd>End</kbd>: moves focus to the last tab
116116
- <kbd>Space</kbd> / <kbd>Enter</kbd>: activates the focused tab (manual mode).
117-
In automatic activation, tabs activate when focused via arrows.
117+
In automatic activation (default), tabs activate when focused via arrows.
118118

119119
#### ARIA implementation
120120

2nd-gen/packages/swc/components/tabs/test/tabs.test.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const OverviewTest: Story = {
5959
'horizontal'
6060
);
6161
expect(tabs.keyboardActivation, 'default keyboard activation').toBe(
62-
'manual'
62+
'automatic'
6363
);
6464
expect(tabs.density, 'default density is regular').toBe('regular');
6565
expect(tabs.disabled, 'default disabled is false').toBe(false);
@@ -288,24 +288,29 @@ export const HostAutoDisabledReactiveTest: Story = {
288288
const tab2 = canvasElement.querySelector('swc-tab[tab-id="2"]') as Tab;
289289

290290
await step(
291-
'manual mode: arrow moves focus without changing selection',
291+
'automatic mode (default): arrow selects focused tab',
292292
async () => {
293-
expect(tabs.keyboardActivation, 'starts in manual mode').toBe('manual');
293+
expect(tabs.keyboardActivation, 'starts in automatic mode').toBe(
294+
'automatic'
295+
);
294296
tab1.focus();
295297
tab1.dispatchEvent(
296298
new KeyboardEvent('keydown', { code: 'ArrowRight', bubbles: true })
297299
);
298300
await tabs.updateComplete;
299301
expect(tabs.ownerDocument.activeElement, 'focus on tab 2').toBe(tab2);
300-
expect(tabs.selected, 'selection unchanged in manual mode').toBe('1');
302+
expect(tabs.selected, 'selection follows focus in automatic mode').toBe(
303+
'2'
304+
);
301305
}
302306
);
303307

304-
await step('automatic mode: arrow then selects focused tab', async () => {
305-
tabs.keyboardActivation = 'automatic';
308+
await step('manual mode: arrow moves focus without selecting', async () => {
309+
tabs.keyboardActivation = 'manual';
310+
tabs.selected = '1';
306311
await tabs.updateComplete;
307-
expect(tabs.keyboardActivation, 'keyboard activation is automatic').toBe(
308-
'automatic'
312+
expect(tabs.keyboardActivation, 'keyboard activation is manual').toBe(
313+
'manual'
309314
);
310315

311316
tab1.focus();
@@ -317,9 +322,7 @@ export const HostAutoDisabledReactiveTest: Story = {
317322
tabs.ownerDocument.activeElement,
318323
'focus on tab 2 after arrow'
319324
).toBe(tab2);
320-
expect(tabs.selected, 'selection follows focus in automatic mode').toBe(
321-
'2'
322-
);
325+
expect(tabs.selected, 'selection unchanged in manual mode').toBe('1');
323326
});
324327

325328
await step(
@@ -900,7 +903,11 @@ export const HomeEndKeyTest: Story = {
900903

901904
export const EnterSpaceActivationTest: Story = {
902905
render: () => html`
903-
<swc-tabs selected="1" accessible-label="Activation test">
906+
<swc-tabs
907+
selected="1"
908+
keyboard-activation="manual"
909+
accessible-label="Activation test"
910+
>
904911
<swc-tab tab-id="1">Tab 1</swc-tab>
905912
<swc-tab tab-id="2">Tab 2</swc-tab>
906913
<swc-tab-panel tab-id="1"><p>Panel 1</p></swc-tab-panel>

CONTRIBUTOR-DOCS/03_project-planning/03_components/tabs/accessibility-migration-analysis.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ This doc describes how `swc-tabs` (with `swc-tab` and `swc-tab-panel`) should be
142142
| [Focus order (WCAG 2.4.3)](https://www.w3.org/TR/WCAG22/#focus-order) | Tab from the tablist should move to meaningful content (`tabpanel` with `tabindex="0"` when needed, or first focusable in panel) per APG guidance. |
143143
| [Name, role, value (WCAG 4.1.2)](https://www.w3.org/TR/WCAG22/#name-role-value) | Each tab needs a name; state (`aria-selected`) must update; panels hidden with `hidden` or equivalent must not leave stale state exposed. |
144144

145-
**Bottom line:** Choose automatic vs manual activation from real performance and DOM shape; implement APG keyboard tables for the chosen model; keep tab↔panel references valid in the composed tree (see Shadow DOM below).
145+
**Bottom line:** Choose automatic vs manual activation from real performance and DOM shape; implement APG keyboard tables for the chosen model; keep tab↔panel references valid in the composed tree (see Shadow DOM below). For `swc-tabs`, that resolved to `automatic` as the default (Q20 in migration-plan.md): panel content is always in the light DOM, and matching React Spectrum/React Aria `Tabs` was prioritized once the APG precondition for automatic activation was confirmed to hold.
146146

147147
---
148148

@@ -167,7 +167,7 @@ This doc describes how `swc-tabs` (with `swc-tab` and `swc-tab-panel`) should be
167167
| **Hidden panels** | Inactive panels use `hidden` (or visibility/display patterns that hide from AT) per APG; do not leave inert content falsely exposed. |
168168
| **`tabindex` on `tabpanel`** | Per [automatic activation example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/), `tabindex="0"` on `tabpanel` helps users move from tablist into panel content when the first element inside is not focusable. [Manual example](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/) may omit `tabpanel` tabindex when the first child is focusable — match APG for your story variants. |
169169
| **Vertical orientation** | If the tablist is vertical, set `aria-orientation="vertical"` and arrow key mapping consistent with APG (Up/Down or Left/Right per spec and docs). |
170-
| **Activation mode (API)** | Mirror 1st-gen `auto` (boolean, default off = manual): `auto` false — arrows move focus only; Enter, Space, or click select; `auto` true — selection follows focus when arrows move between tabs. Document and test both modes; default manual matches lazy or heavy panels. |
170+
| **Activation mode (API)** | Expose both modes via `keyboard-activation`: `manual` — arrows move focus only; Enter, Space, or click select; `automatic` — selection follows focus when arrows move between tabs. Document and test both modes. **Superseded (Q20, see migration-plan.md):** default is `automatic`, not `manual`, to match React Spectrum/React Aria `Tabs``swc-tab-panel` content is always in the light DOM (not lazily mounted), so the "default manual matches lazy or heavy panels" reasoning below does not apply to our default; it's retained as guidance for consumers who lazy-render their own panel content. |
171171

172172
### Shadow DOM and cross-root ARIA Issues
173173

@@ -225,7 +225,7 @@ When implementation gaps appear, classify missing behavior:
225225
## Summary checklist
226226

227227
- [ ] `tablist` / `tab` / `tabpanel` roles and labelling match APG.
228-
- [ ] Automatic vs manual activation documented and tested; default matches content loading model.
228+
- [x] Automatic vs manual activation documented and tested; default is `automatic` (Q20), matching React Spectrum/React Aria `Tabs` and the component's always-in-DOM panel content.
229229
- [ ] Roving `tabindex` and arrows use `FocusgroupNavigationController` where appropriate ([#6129](https://github.com/adobe/spectrum-web-components/pull/6129)); tabs-only logic stays in component unless promoted to shared infra deliberately.
230230
- [ ] `aria-controls` / `aria-labelledby` valid in composed tree (no silent cross-root ID failures).
231231
- [ ] `tabpanel` focus behavior matches APG for focusable vs non-focusable first content.

0 commit comments

Comments
 (0)