diff --git a/packages/widgets/src/docklayout.ts b/packages/widgets/src/docklayout.ts index 70d16e76a..fca1d254c 100644 --- a/packages/widgets/src/docklayout.ts +++ b/packages/widgets/src/docklayout.ts @@ -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); @@ -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; } @@ -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); } /** @@ -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) { @@ -1988,6 +1993,22 @@ namespace Private { } } + export + function addAria(widget: Widget, tabBar: TabBar) { + 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. */ @@ -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. diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index f8fc5f0f9..7cbe87c95 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -32,7 +32,7 @@ import { } from '@lumino/signaling'; import { - ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h + ElementARIAAttrs, ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h } from '@lumino/virtualdom'; import { @@ -65,15 +65,16 @@ class TabBar extends Widget { /* */ this.addClass('p-TabBar'); /* */ + 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; } /** @@ -268,6 +269,25 @@ class TabBar 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. * @@ -296,6 +316,7 @@ class TabBar extends Widget { // Toggle the orientation values. this._orientation = value; this.dataset['orientation'] = value; + this.contentNode.setAttribute('aria-orientation', value); } /** @@ -997,6 +1018,9 @@ class TabBar 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)) { @@ -1050,6 +1074,8 @@ class TabBar 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; @@ -1110,6 +1136,7 @@ class TabBar extends Widget { this.update(); } + private _name: string; private _currentIndex = -1; private _titles: Title[] = []; private _orientation: TabBar.Orientation; @@ -1201,6 +1228,13 @@ namespace TabBar { */ export interface IOptions { + /** + * 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. * @@ -1430,11 +1464,13 @@ namespace TabBar { renderTab(data: IRenderData): 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) @@ -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): ElementARIAAttrs { + return {role: 'tab', 'aria-selected': data.current.toString()}; + } + /** * Create the class name for the tab icon. * @@ -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'; /* */ content.classList.add('p-TabBar-content'); diff --git a/packages/widgets/src/tabpanel.ts b/packages/widgets/src/tabpanel.ts index b6f548ca2..7b324ca1e 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -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); + } } /** @@ -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); }