Skip to content

Commit e22c22f

Browse files
authored
Merge pull request #132 from telamonian/aria-tabs-lumino-update
Add ARIA roles to tabs - lumino update
2 parents da8f700 + a6fdb77 commit e22c22f

File tree

3 files changed

+84
-4
lines changed

3 files changed

+84
-4
lines changed

packages/widgets/src/docklayout.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,8 @@ class DockLayout extends Layout {
639639
return;
640640
}
641641

642+
Private.removeAria(widget);
643+
642644
// If there are multiple tabs, just remove the widget's tab.
643645
if (tabNode.tabBar.titles.length > 1) {
644646
tabNode.tabBar.removeTab(widget.title);
@@ -770,6 +772,7 @@ class DockLayout extends Layout {
770772
let tabNode = new Private.TabLayoutNode(this._createTabBar());
771773
tabNode.tabBar.addTab(widget.title);
772774
this._root = tabNode;
775+
Private.addAria(widget, tabNode.tabBar);
773776
return;
774777
}
775778

@@ -795,6 +798,7 @@ class DockLayout extends Layout {
795798

796799
// Insert the widget's tab relative to the target index.
797800
refNode.tabBar.insertTab(index + (after ? 1 : 0), widget.title);
801+
Private.addAria(widget, refNode.tabBar);
798802
}
799803

800804
/**
@@ -815,6 +819,7 @@ class DockLayout extends Layout {
815819
// Create the tab layout node to hold the widget.
816820
let tabNode = new Private.TabLayoutNode(this._createTabBar());
817821
tabNode.tabBar.addTab(widget.title);
822+
Private.addAria(widget, tabNode.tabBar);
818823

819824
// Set the root if it does not exist.
820825
if (!this._root) {
@@ -1988,6 +1993,22 @@ namespace Private {
19881993
}
19891994
}
19901995

1996+
export
1997+
function addAria(widget: Widget, tabBar: TabBar<Widget>) {
1998+
widget.node.setAttribute('role', 'tabpanel');
1999+
let renderer = tabBar.renderer;
2000+
if (renderer instanceof TabBar.Renderer) {
2001+
let tabId = renderer.createTabKey({ title: widget.title, current: false, zIndex: 0 });
2002+
widget.node.setAttribute('aria-labelledby', tabId);
2003+
}
2004+
}
2005+
2006+
export
2007+
function removeAria(widget: Widget) {
2008+
widget.node.removeAttribute('role');
2009+
widget.node.removeAttribute('aria-labelledby');
2010+
}
2011+
19912012
/**
19922013
* Normalize a tab area config and collect the visited widgets.
19932014
*/
@@ -2077,6 +2098,7 @@ namespace Private {
20772098
each(config.widgets, widget => {
20782099
widget.hide();
20792100
tabBar.addTab(widget.title);
2101+
Private.addAria(widget, tabBar);
20802102
});
20812103

20822104
// Set the current index of the tab bar.

packages/widgets/src/tabbar.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
} from '@lumino/signaling';
3333

3434
import {
35-
ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h
35+
ElementARIAAttrs, ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h
3636
} from '@lumino/virtualdom';
3737

3838
import {
@@ -65,15 +65,16 @@ class TabBar<T> extends Widget {
6565
/* <DEPRECATED> */
6666
this.addClass('p-TabBar');
6767
/* </DEPRECATED> */
68+
this.contentNode.setAttribute('role', 'tablist');
6869
this.setFlag(Widget.Flag.DisallowLayout);
6970
this.tabsMovable = options.tabsMovable || false;
7071
this.titlesEditable = options.titlesEditable || false;
7172
this.allowDeselect = options.allowDeselect || false;
7273
this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
74+
this.name = options.name || '';
75+
this.orientation = options.orientation || 'horizontal';
7376
this.removeBehavior = options.removeBehavior || 'select-tab-after';
7477
this.renderer = options.renderer || TabBar.defaultRenderer;
75-
this._orientation = options.orientation || 'horizontal';
76-
this.dataset['orientation'] = this._orientation;
7778
}
7879

7980
/**
@@ -268,6 +269,25 @@ class TabBar<T> extends Widget {
268269
});
269270
}
270271

272+
/**
273+
* Get the name of the tab bar.
274+
*/
275+
get name(): string {
276+
return this._name;
277+
}
278+
279+
/**
280+
* Set the name of the tab bar.
281+
*/
282+
set name(value: string) {
283+
this._name = value;
284+
if (value) {
285+
this.contentNode.setAttribute('aria-label', value);
286+
} else {
287+
this.contentNode.removeAttribute('aria-label');
288+
}
289+
}
290+
271291
/**
272292
* Get the orientation of the tab bar.
273293
*
@@ -296,6 +316,7 @@ class TabBar<T> extends Widget {
296316
// Toggle the orientation values.
297317
this._orientation = value;
298318
this.dataset['orientation'] = value;
319+
this.contentNode.setAttribute('aria-orientation', value);
299320
}
300321

301322
/**
@@ -997,6 +1018,9 @@ class TabBar<T> extends Widget {
9971018
let ci = this._currentIndex;
9981019
let bh = this.insertBehavior;
9991020

1021+
1022+
// TODO: do we need to do an update to update the aria-selected attribute?
1023+
10001024
// Handle the behavior where the new tab is always selected,
10011025
// or the behavior where the new tab is selected if needed.
10021026
if (bh === 'select-tab' || (bh === 'select-tab-if-needed' && ci === -1)) {
@@ -1050,6 +1074,8 @@ class TabBar<T> extends Widget {
10501074
return;
10511075
}
10521076

1077+
// TODO: do we need to do an update to adjust the aria-selected value?
1078+
10531079
// No tab gets selected if the tab bar is empty.
10541080
if (this._titles.length === 0) {
10551081
this._currentIndex = -1;
@@ -1110,6 +1136,7 @@ class TabBar<T> extends Widget {
11101136
this.update();
11111137
}
11121138

1139+
private _name: string;
11131140
private _currentIndex = -1;
11141141
private _titles: Title<T>[] = [];
11151142
private _orientation: TabBar.Orientation;
@@ -1201,6 +1228,13 @@ namespace TabBar {
12011228
*/
12021229
export
12031230
interface IOptions<T> {
1231+
/**
1232+
* Name of the tab bar.
1233+
*
1234+
* This is used for accessibility reasons. The default is the empty string.
1235+
*/
1236+
name?: string;
1237+
12041238
/**
12051239
* The layout orientation of the tab bar.
12061240
*
@@ -1430,11 +1464,13 @@ namespace TabBar {
14301464
renderTab(data: IRenderData<any>): VirtualElement {
14311465
let title = data.title.caption;
14321466
let key = this.createTabKey(data);
1467+
let id = key;
14331468
let style = this.createTabStyle(data);
14341469
let className = this.createTabClass(data);
14351470
let dataset = this.createTabDataset(data);
1471+
let aria = this.createTabARIA(data);
14361472
return (
1437-
h.li({ key, className, title, style, dataset },
1473+
h.li({ id, key, className, title, style, dataset, ...aria },
14381474
this.renderIcon(data),
14391475
this.renderLabel(data),
14401476
this.renderCloseIcon(data)
@@ -1568,6 +1604,17 @@ namespace TabBar {
15681604
return data.title.dataset;
15691605
}
15701606

1607+
/**
1608+
* Create the ARIA attributes for a tab.
1609+
*
1610+
* @param data - The data to use for the tab.
1611+
*
1612+
* @returns The ARIA attributes for the tab.
1613+
*/
1614+
createTabARIA(data: IRenderData<any>): ElementARIAAttrs {
1615+
return {role: 'tab', 'aria-selected': data.current.toString()};
1616+
}
1617+
15711618
/**
15721619
* Create the class name for the tab icon.
15731620
*
@@ -1730,6 +1777,7 @@ namespace Private {
17301777
function createNode(): HTMLDivElement {
17311778
let node = document.createElement('div');
17321779
let content = document.createElement('ul');
1780+
content.setAttribute('role', 'tablist');
17331781
content.className = 'lm-TabBar-content';
17341782
/* <DEPRECATED> */
17351783
content.classList.add('p-TabBar-content');

packages/widgets/src/tabpanel.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,14 @@ class TabPanel extends Widget {
272272
}
273273
this.stackedPanel.insertWidget(index, widget);
274274
this.tabBar.insertTab(index, widget.title);
275+
276+
widget.node.setAttribute('role', 'tabpanel');
277+
278+
let renderer = this.tabBar.renderer
279+
if (renderer instanceof TabBar.Renderer) {
280+
let tabId = renderer.createTabKey({title: widget.title, current: false, zIndex: 0});
281+
widget.node.setAttribute('aria-labelledby', tabId);
282+
}
275283
}
276284

277285
/**
@@ -331,6 +339,8 @@ class TabPanel extends Widget {
331339
* Handle the `widgetRemoved` signal from the stacked panel.
332340
*/
333341
private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
342+
widget.node.removeAttribute('role');
343+
widget.node.removeAttribute('aria-labelledby');
334344
this.tabBar.removeTab(widget.title);
335345
}
336346

0 commit comments

Comments
 (0)