Skip to content

Commit

Permalink
Merge pull request #132 from telamonian/aria-tabs-lumino-update
Browse files Browse the repository at this point in the history
Add ARIA roles to tabs - lumino update
  • Loading branch information
blink1073 authored Mar 11, 2021
2 parents da8f700 + a6fdb77 commit e22c22f
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 4 deletions.
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

0 comments on commit e22c22f

Please sign in to comment.