Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a1a087f

Browse files
authored
Fix usages of ARIA tabpanel (#10628)
* RovingTabIndex handle looping around start/end * Make TabbedView expose aria tabpanel/tablist/tab roles * Fix right panel being wrongly specified as aria tabs Not all right panels map to the top right header buttons so we cannot describe it as a tabpanel relation * tsc strict * Update snapshots * Fix ARIA AXE violation * Update tests
1 parent 961b843 commit a1a087f

File tree

9 files changed

+153
-66
lines changed

9 files changed

+153
-66
lines changed

cypress/e2e/integration-manager/get-openid-token.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const INTEGRATION_MANAGER_HTML = `
5959
`;
6060

6161
function openIntegrationManager() {
62-
cy.findByRole("tab", { name: "Room info" }).click();
62+
cy.findByRole("button", { name: "Room info" }).click();
6363
cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
6464
}
6565

cypress/e2e/integration-manager/kick.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const INTEGRATION_MANAGER_HTML = `
6262
`;
6363

6464
function openIntegrationManager() {
65-
cy.findByRole("tab", { name: "Room info" }).click();
65+
cy.findByRole("button", { name: "Room info" }).click();
6666
cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
6767
}
6868

src/accessibility/RovingTabIndex.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export const reducer: Reducer<IState, IAction> = (state: IState, action: IAction
156156
};
157157

158158
interface IProps {
159+
handleLoop?: boolean;
159160
handleHomeEnd?: boolean;
160161
handleUpDown?: boolean;
161162
handleLeftRight?: boolean;
@@ -167,19 +168,26 @@ export const findSiblingElement = (
167168
refs: RefObject<HTMLElement>[],
168169
startIndex: number,
169170
backwards = false,
171+
loop = false,
170172
): RefObject<HTMLElement> | undefined => {
171173
if (backwards) {
172174
for (let i = startIndex; i < refs.length && i >= 0; i--) {
173175
if (refs[i].current?.offsetParent !== null) {
174176
return refs[i];
175177
}
176178
}
179+
if (loop) {
180+
return findSiblingElement(refs.slice(startIndex + 1), refs.length - 1, true, false);
181+
}
177182
} else {
178183
for (let i = startIndex; i < refs.length && i >= 0; i++) {
179184
if (refs[i].current?.offsetParent !== null) {
180185
return refs[i];
181186
}
182187
}
188+
if (loop) {
189+
return findSiblingElement(refs.slice(0, startIndex), 0, false, false);
190+
}
183191
}
184192
};
185193

@@ -188,6 +196,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
188196
handleHomeEnd,
189197
handleUpDown,
190198
handleLeftRight,
199+
handleLoop,
191200
onKeyDown,
192201
}) => {
193202
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
@@ -252,7 +261,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
252261
handled = true;
253262
if (context.state.refs.length > 0) {
254263
const idx = context.state.refs.indexOf(context.state.activeRef!);
255-
focusRef = findSiblingElement(context.state.refs, idx + 1);
264+
focusRef = findSiblingElement(context.state.refs, idx + 1, false, handleLoop);
256265
}
257266
}
258267
break;
@@ -266,7 +275,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
266275
handled = true;
267276
if (context.state.refs.length > 0) {
268277
const idx = context.state.refs.indexOf(context.state.activeRef!);
269-
focusRef = findSiblingElement(context.state.refs, idx - 1, true);
278+
focusRef = findSiblingElement(context.state.refs, idx - 1, true, handleLoop);
270279
}
271280
}
272281
break;
@@ -289,7 +298,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({
289298
});
290299
}
291300
},
292-
[context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight],
301+
[context, onKeyDown, handleHomeEnd, handleUpDown, handleLeftRight, handleLoop],
293302
);
294303

295304
return (

src/components/structures/TabbedView.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import { logger } from "matrix-js-sdk/src/logger";
2222

2323
import { _t } from "../../languageHandler";
2424
import AutoHideScrollbar from "./AutoHideScrollbar";
25-
import AccessibleButton from "../views/elements/AccessibleButton";
2625
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
2726
import { NonEmptyArray } from "../../@types/common";
27+
import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
2828

2929
/**
3030
* Represents a tab for the TabbedView.
@@ -98,34 +98,46 @@ export default class TabbedView extends React.Component<IProps, IState> {
9898
}
9999

100100
private renderTabLabel(tab: Tab): JSX.Element {
101-
let classes = "mx_TabbedView_tabLabel ";
102-
103-
if (this.state.activeTabId === tab.id) classes += "mx_TabbedView_tabLabel_active";
101+
const isActive = this.state.activeTabId === tab.id;
102+
const classes = classNames("mx_TabbedView_tabLabel", {
103+
mx_TabbedView_tabLabel_active: isActive,
104+
});
104105

105106
let tabIcon: JSX.Element | undefined;
106107
if (tab.icon) {
107108
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
108109
}
109110

110111
const onClickHandler = (): void => this.setActiveTab(tab);
112+
const id = this.getTabId(tab);
111113

112114
const label = _t(tab.label);
113115
return (
114-
<AccessibleButton
116+
<RovingAccessibleButton
115117
className={classes}
116118
key={"tab_label_" + tab.label}
117119
onClick={onClickHandler}
118120
data-testid={`settings-tab-${tab.id}`}
121+
role="tab"
122+
aria-selected={isActive}
123+
aria-controls={id}
119124
>
120125
{tabIcon}
121-
<span className="mx_TabbedView_tabLabel_text">{label}</span>
122-
</AccessibleButton>
126+
<span className="mx_TabbedView_tabLabel_text" id={`${id}_label`}>
127+
{label}
128+
</span>
129+
</RovingAccessibleButton>
123130
);
124131
}
125132

133+
private getTabId(tab: Tab): string {
134+
return `mx_tabpanel_${tab.id}`;
135+
}
136+
126137
private renderTabPanel(tab: Tab): React.ReactNode {
138+
const id = this.getTabId(tab);
127139
return (
128-
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
140+
<div className="mx_TabbedView_tabPanel" key={id} id={id} aria-labelledby={`${id}_label`}>
129141
<AutoHideScrollbar className="mx_TabbedView_tabPanelContent">{tab.body}</AutoHideScrollbar>
130142
</div>
131143
);
@@ -147,7 +159,23 @@ export default class TabbedView extends React.Component<IProps, IState> {
147159
return (
148160
<div className={tabbedViewClasses}>
149161
{screenName && <PosthogScreenTracker screenName={screenName} />}
150-
<div className="mx_TabbedView_tabLabels">{labels}</div>
162+
<RovingTabIndexProvider
163+
handleLoop
164+
handleHomeEnd
165+
handleLeftRight={this.props.tabLocation == TabLocation.TOP}
166+
handleUpDown={this.props.tabLocation == TabLocation.LEFT}
167+
>
168+
{({ onKeyDownHandler }) => (
169+
<div
170+
className="mx_TabbedView_tabLabels"
171+
role="tablist"
172+
aria-orientation={this.props.tabLocation == TabLocation.LEFT ? "vertical" : "horizontal"}
173+
onKeyDown={onKeyDownHandler}
174+
>
175+
{labels}
176+
</div>
177+
)}
178+
</RovingTabIndexProvider>
151179
{panel}
152180
</div>
153181
);

src/components/views/right_panel/HeaderButton.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ export default class HeaderButton extends React.Component<IProps> {
5454
return (
5555
<AccessibleTooltipButton
5656
{...props}
57-
aria-selected={isHighlighted}
58-
role="tab"
57+
aria-current={isHighlighted ? "true" : "false"}
5958
title={title}
6059
alignment={Alignment.Bottom}
6160
className={classes}

src/components/views/right_panel/HeaderButtons.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
9898
public abstract renderButtons(): JSX.Element;
9999

100100
public render(): React.ReactNode {
101-
return (
102-
<div className="mx_HeaderButtons" role="tablist">
103-
{this.renderButtons()}
104-
</div>
105-
);
101+
return <div className="mx_HeaderButtons">{this.renderButtons()}</div>;
106102
}
107103
}

test/components/structures/__snapshots__/TabbedView-test.tsx.snap

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,69 @@ exports[`<TabbedView /> renders tabs 1`] = `
66
class="mx_TabbedView mx_TabbedView_tabsOnLeft"
77
>
88
<div
9+
aria-orientation="vertical"
910
class="mx_TabbedView_tabLabels"
11+
role="tablist"
1012
>
1113
<div
14+
aria-controls="mx_tabpanel_GENERAL"
15+
aria-selected="true"
1216
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
1317
data-testid="settings-tab-GENERAL"
14-
role="button"
18+
role="tab"
1519
tabindex="0"
1620
>
1721
<span
1822
class="mx_TabbedView_maskedIcon general"
1923
/>
2024
<span
2125
class="mx_TabbedView_tabLabel_text"
26+
id="mx_tabpanel_GENERAL_label"
2227
>
2328
General
2429
</span>
2530
</div>
2631
<div
27-
class="mx_AccessibleButton mx_TabbedView_tabLabel "
32+
aria-controls="mx_tabpanel_LABS"
33+
aria-selected="false"
34+
class="mx_AccessibleButton mx_TabbedView_tabLabel"
2835
data-testid="settings-tab-LABS"
29-
role="button"
30-
tabindex="0"
36+
role="tab"
37+
tabindex="-1"
3138
>
3239
<span
3340
class="mx_TabbedView_maskedIcon labs"
3441
/>
3542
<span
3643
class="mx_TabbedView_tabLabel_text"
44+
id="mx_tabpanel_LABS_label"
3745
>
3846
Labs
3947
</span>
4048
</div>
4149
<div
42-
class="mx_AccessibleButton mx_TabbedView_tabLabel "
50+
aria-controls="mx_tabpanel_SECURITY"
51+
aria-selected="false"
52+
class="mx_AccessibleButton mx_TabbedView_tabLabel"
4353
data-testid="settings-tab-SECURITY"
44-
role="button"
45-
tabindex="0"
54+
role="tab"
55+
tabindex="-1"
4656
>
4757
<span
4858
class="mx_TabbedView_maskedIcon security"
4959
/>
5060
<span
5161
class="mx_TabbedView_tabLabel_text"
62+
id="mx_tabpanel_SECURITY_label"
5263
>
5364
Security
5465
</span>
5566
</div>
5667
</div>
5768
<div
69+
aria-labelledby="mx_tabpanel_GENERAL_label"
5870
class="mx_TabbedView_tabPanel"
71+
id="mx_tabpanel_GENERAL"
5972
>
6073
<div
6174
class="mx_AutoHideScrollbar mx_TabbedView_tabPanelContent"

test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,91 @@
33
exports[`<RoomSettingsDialog /> Settings tabs renders default tabs correctly 1`] = `
44
NodeList [
55
<div
6+
aria-controls="mx_tabpanel_ROOM_GENERAL_TAB"
7+
aria-selected="true"
68
class="mx_AccessibleButton mx_TabbedView_tabLabel mx_TabbedView_tabLabel_active"
79
data-testid="settings-tab-ROOM_GENERAL_TAB"
8-
role="button"
10+
role="tab"
911
tabindex="0"
1012
>
1113
<span
1214
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_settingsIcon"
1315
/>
1416
<span
1517
class="mx_TabbedView_tabLabel_text"
18+
id="mx_tabpanel_ROOM_GENERAL_TAB_label"
1619
>
1720
General
1821
</span>
1922
</div>,
2023
<div
21-
class="mx_AccessibleButton mx_TabbedView_tabLabel "
24+
aria-controls="mx_tabpanel_ROOM_SECURITY_TAB"
25+
aria-selected="false"
26+
class="mx_AccessibleButton mx_TabbedView_tabLabel"
2227
data-testid="settings-tab-ROOM_SECURITY_TAB"
23-
role="button"
24-
tabindex="0"
28+
role="tab"
29+
tabindex="-1"
2530
>
2631
<span
2732
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_securityIcon"
2833
/>
2934
<span
3035
class="mx_TabbedView_tabLabel_text"
36+
id="mx_tabpanel_ROOM_SECURITY_TAB_label"
3137
>
3238
Security & Privacy
3339
</span>
3440
</div>,
3541
<div
36-
class="mx_AccessibleButton mx_TabbedView_tabLabel "
42+
aria-controls="mx_tabpanel_ROOM_ROLES_TAB"
43+
aria-selected="false"
44+
class="mx_AccessibleButton mx_TabbedView_tabLabel"
3745
data-testid="settings-tab-ROOM_ROLES_TAB"
38-
role="button"
39-
tabindex="0"
46+
role="tab"
47+
tabindex="-1"
4048
>
4149
<span
4250
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_rolesIcon"
4351
/>
4452
<span
4553
class="mx_TabbedView_tabLabel_text"
54+
id="mx_tabpanel_ROOM_ROLES_TAB_label"
4655
>
4756
Roles & Permissions
4857
</span>
4958
</div>,
5059
<div
51-
class="mx_AccessibleButton mx_TabbedView_tabLabel "
60+
aria-controls="mx_tabpanel_ROOM_NOTIFICATIONS_TAB"
61+
aria-selected="false"
62+
class="mx_AccessibleButton mx_TabbedView_tabLabel"
5263
data-testid="settings-tab-ROOM_NOTIFICATIONS_TAB"
53-
role="button"
54-
tabindex="0"
64+
role="tab"
65+
tabindex="-1"
5566
>
5667
<span
5768
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_notificationsIcon"
5869
/>
5970
<span
6071
class="mx_TabbedView_tabLabel_text"
72+
id="mx_tabpanel_ROOM_NOTIFICATIONS_TAB_label"
6173
>
6274
Notifications
6375
</span>
6476
</div>,
6577
<div
66-
class="mx_AccessibleButton mx_TabbedView_tabLabel "
78+
aria-controls="mx_tabpanel_ROOM_POLL_HISTORY_TAB"
79+
aria-selected="false"
80+
class="mx_AccessibleButton mx_TabbedView_tabLabel"
6781
data-testid="settings-tab-ROOM_POLL_HISTORY_TAB"
68-
role="button"
69-
tabindex="0"
82+
role="tab"
83+
tabindex="-1"
7084
>
7185
<span
7286
class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_pollsIcon"
7387
/>
7488
<span
7589
class="mx_TabbedView_tabLabel_text"
90+
id="mx_tabpanel_ROOM_POLL_HISTORY_TAB_label"
7691
>
7792
Poll history
7893
</span>

0 commit comments

Comments
 (0)