Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ARIA roles to tabs - lumino update #132

Merged
merged 14 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/widgets/src/docklayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,8 @@ class DockLayout extends Layout {
return;
}

Private.removeAria(widget);

// If there are multiple tabs, just remove the widget's tab.
if (tabNode.tabBar.titles.length > 1) {
tabNode.tabBar.removeTab(widget.title);
Expand Down Expand Up @@ -770,6 +772,7 @@ class DockLayout extends Layout {
let tabNode = new Private.TabLayoutNode(this._createTabBar());
tabNode.tabBar.addTab(widget.title);
this._root = tabNode;
Private.addAria(widget, tabNode.tabBar);
return;
}

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

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

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

// Set the root if it does not exist.
if (!this._root) {
Expand Down Expand Up @@ -1988,6 +1993,22 @@ namespace Private {
}
}

export
function addAria(widget: Widget, tabBar: TabBar<Widget>) {
widget.node.setAttribute('role', 'tabpanel');
let renderer = tabBar.renderer;
if (renderer instanceof TabBar.Renderer) {
let tabId = renderer.createTabKey({ title: widget.title, current: false, zIndex: 0 });
widget.node.setAttribute('aria-labelledby', tabId);
}
}

export
function removeAria(widget: Widget) {
widget.node.removeAttribute('role');
widget.node.removeAttribute('aria-labelledby');
}

/**
* Normalize a tab area config and collect the visited widgets.
*/
Expand Down Expand Up @@ -2077,6 +2098,7 @@ namespace Private {
each(config.widgets, widget => {
widget.hide();
tabBar.addTab(widget.title);
Private.addAria(widget, tabBar);
});

// Set the current index of the tab bar.
Expand Down
56 changes: 52 additions & 4 deletions packages/widgets/src/tabbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from '@lumino/signaling';

import {
ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h
ElementARIAAttrs, ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h
} from '@lumino/virtualdom';

import {
Expand Down Expand Up @@ -65,15 +65,16 @@ class TabBar<T> extends Widget {
/* <DEPRECATED> */
this.addClass('p-TabBar');
/* </DEPRECATED> */
this.contentNode.setAttribute('role', 'tablist');
this.setFlag(Widget.Flag.DisallowLayout);
this.tabsMovable = options.tabsMovable || false;
this.titlesEditable = options.titlesEditable || false;
this.allowDeselect = options.allowDeselect || false;
this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
this.name = options.name || '';
this.orientation = options.orientation || 'horizontal';
this.removeBehavior = options.removeBehavior || 'select-tab-after';
this.renderer = options.renderer || TabBar.defaultRenderer;
this._orientation = options.orientation || 'horizontal';
this.dataset['orientation'] = this._orientation;
}

/**
Expand Down Expand Up @@ -268,6 +269,25 @@ class TabBar<T> extends Widget {
});
}

/**
* Get the name of the tab bar.
*/
get name(): string {
return this._name;
}

/**
* Set the name of the tab bar.
*/
set name(value: string) {
this._name = value;
if (value) {
this.contentNode.setAttribute('aria-label', value);
} else {
this.contentNode.removeAttribute('aria-label');
}
}

/**
* Get the orientation of the tab bar.
*
Expand Down Expand Up @@ -296,6 +316,7 @@ class TabBar<T> extends Widget {
// Toggle the orientation values.
this._orientation = value;
this.dataset['orientation'] = value;
this.contentNode.setAttribute('aria-orientation', value);
}

/**
Expand Down Expand Up @@ -997,6 +1018,9 @@ class TabBar<T> extends Widget {
let ci = this._currentIndex;
let bh = this.insertBehavior;


// TODO: do we need to do an update to update the aria-selected attribute?

// Handle the behavior where the new tab is always selected,
// or the behavior where the new tab is selected if needed.
if (bh === 'select-tab' || (bh === 'select-tab-if-needed' && ci === -1)) {
Expand Down Expand Up @@ -1050,6 +1074,8 @@ class TabBar<T> extends Widget {
return;
}

// TODO: do we need to do an update to adjust the aria-selected value?

// No tab gets selected if the tab bar is empty.
if (this._titles.length === 0) {
this._currentIndex = -1;
Expand Down Expand Up @@ -1110,6 +1136,7 @@ class TabBar<T> extends Widget {
this.update();
}

private _name: string;
private _currentIndex = -1;
private _titles: Title<T>[] = [];
private _orientation: TabBar.Orientation;
Expand Down Expand Up @@ -1201,6 +1228,13 @@ namespace TabBar {
*/
export
interface IOptions<T> {
/**
* Name of the tab bar.
*
* This is used for accessibility reasons. The default is the empty string.
*/
name?: string;

/**
* The layout orientation of the tab bar.
*
Expand Down Expand Up @@ -1430,11 +1464,13 @@ namespace TabBar {
renderTab(data: IRenderData<any>): VirtualElement {
let title = data.title.caption;
let key = this.createTabKey(data);
let id = key;
let style = this.createTabStyle(data);
let className = this.createTabClass(data);
let dataset = this.createTabDataset(data);
let aria = this.createTabARIA(data);
return (
h.li({ key, className, title, style, dataset },
h.li({ id, key, className, title, style, dataset, ...aria },
this.renderIcon(data),
this.renderLabel(data),
this.renderCloseIcon(data)
Expand Down Expand Up @@ -1568,6 +1604,17 @@ namespace TabBar {
return data.title.dataset;
}

/**
* Create the ARIA attributes for a tab.
*
* @param data - The data to use for the tab.
*
* @returns The ARIA attributes for the tab.
*/
createTabARIA(data: IRenderData<any>): ElementARIAAttrs {
return {role: 'tab', 'aria-selected': data.current.toString()};
}

/**
* Create the class name for the tab icon.
*
Expand Down Expand Up @@ -1730,6 +1777,7 @@ namespace Private {
function createNode(): HTMLDivElement {
let node = document.createElement('div');
let content = document.createElement('ul');
content.setAttribute('role', 'tablist');
content.className = 'lm-TabBar-content';
/* <DEPRECATED> */
content.classList.add('p-TabBar-content');
Expand Down
10 changes: 10 additions & 0 deletions packages/widgets/src/tabpanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,14 @@ class TabPanel extends Widget {
}
this.stackedPanel.insertWidget(index, widget);
this.tabBar.insertTab(index, widget.title);

widget.node.setAttribute('role', 'tabpanel');

let renderer = this.tabBar.renderer
if (renderer instanceof TabBar.Renderer) {
let tabId = renderer.createTabKey({title: widget.title, current: false, zIndex: 0});
widget.node.setAttribute('aria-labelledby', tabId);
}
}

/**
Expand Down Expand Up @@ -331,6 +339,8 @@ class TabPanel extends Widget {
* Handle the `widgetRemoved` signal from the stacked panel.
*/
private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
widget.node.removeAttribute('role');
widget.node.removeAttribute('aria-labelledby');
this.tabBar.removeTab(widget.title);
}

Expand Down