From 0f73321b7dd6db07017b80cbdfa83495107cb424 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 20:21:46 -0700 Subject: [PATCH 01/14] Add tab and tablist ARIA attributes for tabs. --- packages/widgets/src/tabbar.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index f8fc5f0f9..991d2d536 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 { @@ -1433,8 +1433,9 @@ namespace TabBar { 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({ key, className, title, style, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data), this.renderCloseIcon(data) @@ -1568,6 +1569,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'}; + } + /** * Create the class name for the tab icon. * @@ -1730,6 +1742,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'); From 67cfd6fb45467feddd5d734dcd44092282a07a27 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 21:16:40 -0700 Subject: [PATCH 02/14] Add tab aria attributes in constructor. --- packages/widgets/src/tabbar.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 991d2d536..63ec0a608 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -74,6 +74,12 @@ class TabBar extends Widget { this.renderer = options.renderer || TabBar.defaultRenderer; this._orientation = options.orientation || 'horizontal'; this.dataset['orientation'] = this._orientation; + + // Should tablist be on the contentNode, or on this.node? (the div or the ul + // containing the li elements?) + let contentNode = this.contentNode; + contentNode.setAttribute('role', 'tablist'); + contentNode.setAttribute('aria-orientation', this.orientation); } /** From 6ffad17525e413b7232376eff2bd91b539a2d3f9 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 03:02:40 -0700 Subject: [PATCH 03/14] Initial draft of adding tabpanel aria data for tabpanel and dockpanel. --- packages/widgets/src/docklayout.ts | 22 +++++++++++++++++++ packages/widgets/src/tabbar.ts | 34 +++++++++++++++++------------- packages/widgets/src/tabpanel.ts | 14 ++++++++++++ 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/widgets/src/docklayout.ts b/packages/widgets/src/docklayout.ts index 70d16e76a..48583fc0c 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 + async function addAria(widget: Widget, tabBar: TabBar) { + let tabId = tabBar.renderer.createTabKey({title: widget.title, current: false, zIndex: 0}); + + if (tabId) { + widget.node.setAttribute('role', 'tabpanel'); + widget.node.setAttribute('aria-labelledby', tabId); + } + } + + export + async 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 63ec0a608..9a594ce01 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -53,7 +53,7 @@ import { * property should be set to `false` when rotating nodes from CSS. */ export -class TabBar extends Widget { +class TabBar extends Widget { /** * Construct a new tab bar. * @@ -302,6 +302,7 @@ class TabBar extends Widget { // Toggle the orientation values. this._orientation = value; this.dataset['orientation'] = value; + this.contentNode.setAttribute('aria-orientation', value); } /** @@ -1406,6 +1407,8 @@ namespace TabBar { * @returns A virtual element representing the tab. */ renderTab(data: IRenderData): VirtualElement; + + createTabKey(data: IRenderData): string; } /** @@ -1415,7 +1418,7 @@ namespace TabBar { * Subclasses are free to reimplement rendering methods as needed. */ export - class Renderer implements IRenderer { + class Renderer implements IRenderer { /** * Construct a new renderer. */ @@ -1433,15 +1436,16 @@ namespace TabBar { * * @returns A virtual element representing the tab. */ - renderTab(data: IRenderData): VirtualElement { + 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, ...aria }, + h.li({ id, key, className, title, style, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data), this.renderCloseIcon(data) @@ -1456,7 +1460,7 @@ namespace TabBar { * * @returns A virtual element representing the tab icon. */ - renderIcon(data: IRenderData): VirtualElement { + renderIcon(data: IRenderData): VirtualElement { const { title } = data; let className = this.createIconClass(data); @@ -1477,7 +1481,7 @@ namespace TabBar { * * @returns A virtual element representing the tab label. */ - renderLabel(data: IRenderData): VirtualElement { + renderLabel(data: IRenderData): VirtualElement { return h.div({ className: 'lm-TabBar-tabLabel' /* */ @@ -1493,7 +1497,7 @@ namespace TabBar { * * @returns A virtual element representing the tab close icon. */ - renderCloseIcon(data: IRenderData): VirtualElement { + renderCloseIcon(data: IRenderData): VirtualElement { return h.div({ className: 'lm-TabBar-tabCloseIcon' /* */ @@ -1514,7 +1518,7 @@ namespace TabBar { * the key is generated. This enables efficient rendering of moved * tabs and avoids subtle hover style artifacts. */ - createTabKey(data: IRenderData): string { + createTabKey(data: IRenderData): string { let key = this._tabKeys.get(data.title); if (key === undefined) { key = `tab-key-${this._tabID++}`; @@ -1530,7 +1534,7 @@ namespace TabBar { * * @returns The inline style data for the tab. */ - createTabStyle(data: IRenderData): ElementInlineStyle { + createTabStyle(data: IRenderData): ElementInlineStyle { return { zIndex: `${data.zIndex}` }; } @@ -1541,7 +1545,7 @@ namespace TabBar { * * @returns The full class name for the tab. */ - createTabClass(data: IRenderData): string { + createTabClass(data: IRenderData): string { let name = 'lm-TabBar-tab'; /* */ name += ' p-TabBar-tab'; @@ -1571,7 +1575,7 @@ namespace TabBar { * * @returns The dataset for the tab. */ - createTabDataset(data: IRenderData): ElementDataset { + createTabDataset(data: IRenderData): ElementDataset { return data.title.dataset; } @@ -1582,8 +1586,8 @@ namespace TabBar { * * @returns The ARIA attributes for the tab. */ - createTabARIA(data: IRenderData): ElementARIAAttrs { - return {role: 'tab'}; + createTabARIA(data: IRenderData): ElementARIAAttrs { + return {role: 'tab', 'aria-controls': data.title.owner.id}; } /** @@ -1593,7 +1597,7 @@ namespace TabBar { * * @returns The full class name for the tab icon. */ - createIconClass(data: IRenderData): string { + createIconClass(data: IRenderData): string { let name = 'lm-TabBar-tabIcon'; /* */ name += ' p-TabBar-tabIcon'; @@ -1603,7 +1607,7 @@ namespace TabBar { } private _tabID = 0; - private _tabKeys = new WeakMap, string>(); + private _tabKeys = new WeakMap, string>(); } /** diff --git a/packages/widgets/src/tabpanel.ts b/packages/widgets/src/tabpanel.ts index b6f548ca2..e084b6ec5 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -34,6 +34,7 @@ import { import { Widget } from './widget'; +import { UUID } from '@phosphor/coreutils'; /** @@ -270,8 +271,16 @@ class TabPanel extends Widget { if (widget !== this.currentWidget) { widget.hide(); } + + widget.id = widget.id || `aria-${UUID.uuid4()}`; + this.stackedPanel.insertWidget(index, widget); this.tabBar.insertTab(index, widget.title); + + let tab = this.tabBar.contentNode.children[this.tabBar.titles.indexOf(widget.title)]; + + widget.node.setAttribute('role', 'tabpanel'); + widget.node.setAttribute('aria-labelledby', tab.id); } /** @@ -331,6 +340,11 @@ 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'); + if (widget.id.slice(5) === 'aria-') { + widget.id = ''; + } this.tabBar.removeTab(widget.title); } From 943e83e1d932eba14b5c960cf541b587e8bcdbcd Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 03:53:05 -0700 Subject: [PATCH 04/14] Add aria-label and aria-selected to tab bars. --- packages/widgets/src/tabbar.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 9a594ce01..70aac80eb 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -80,6 +80,9 @@ class TabBar extends Widget { let contentNode = this.contentNode; contentNode.setAttribute('role', 'tablist'); contentNode.setAttribute('aria-orientation', this.orientation); + + // TODO: what should this be? + contentNode.setAttribute('aria-label', 'Tabs'); } /** @@ -1587,7 +1590,7 @@ namespace TabBar { * @returns The ARIA attributes for the tab. */ createTabARIA(data: IRenderData): ElementARIAAttrs { - return {role: 'tab', 'aria-controls': data.title.owner.id}; + return {role: 'tab', 'aria-controls': data.title.owner.id, 'aria-selected': data.current.toString()}; } /** From 7f39ac92e5008daded5aa0500bb959dc96de1fae Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 14:08:35 -0700 Subject: [PATCH 05/14] Remove aria-controls. According to experts (including both an invited expert advisor and a co-chair of ARIA committee), aria-controls is not needed, and actually annoying in JAWS. --- packages/widgets/src/tabbar.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 70aac80eb..b3b342323 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -75,8 +75,6 @@ class TabBar extends Widget { this._orientation = options.orientation || 'horizontal'; this.dataset['orientation'] = this._orientation; - // Should tablist be on the contentNode, or on this.node? (the div or the ul - // containing the li elements?) let contentNode = this.contentNode; contentNode.setAttribute('role', 'tablist'); contentNode.setAttribute('aria-orientation', this.orientation); @@ -1590,7 +1588,7 @@ namespace TabBar { * @returns The ARIA attributes for the tab. */ createTabARIA(data: IRenderData): ElementARIAAttrs { - return {role: 'tab', 'aria-controls': data.title.owner.id, 'aria-selected': data.current.toString()}; + return {role: 'tab', 'aria-selected': data.current.toString()}; } /** From 38a23b0e4be55098cb4e57584113f1cef1880b3e Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 16:36:54 -0700 Subject: [PATCH 06/14] =?UTF-8?q?Add=20tab=20bar=20names,=20and=20default?= =?UTF-8?q?=20to=20=E2=80=9CActivities=20=E2=80=9D=20for=20dockpan?= =?UTF-8?q?el.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also use the tab bar orientation setter to set the default orientation. --- packages/widgets/src/dockpanel.ts | 3 ++- packages/widgets/src/tabbar.ts | 39 ++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/widgets/src/dockpanel.ts b/packages/widgets/src/dockpanel.ts index 593000d81..98a4031ee 100644 --- a/packages/widgets/src/dockpanel.ts +++ b/packages/widgets/src/dockpanel.ts @@ -1299,7 +1299,7 @@ namespace DockPanel { * @returns A new tab bar for a dock panel. */ createTabBar(): TabBar { - let bar = new TabBar(); + let bar = new TabBar({name: `Activities ${this._tabBarCounter++}`}); bar.addClass('lm-DockPanel-tabBar'); /* */ bar.addClass('p-DockPanel-tabBar'); @@ -1320,6 +1320,7 @@ namespace DockPanel { /* */; return handle; } + private _tabBarCounter = 0; } /** diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index b3b342323..6f5076df7 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -65,22 +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; - - let contentNode = this.contentNode; - contentNode.setAttribute('role', 'tablist'); - contentNode.setAttribute('aria-orientation', this.orientation); - - // TODO: what should this be? - contentNode.setAttribute('aria-label', 'Tabs'); } /** @@ -275,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. * @@ -1118,6 +1131,7 @@ class TabBar extends Widget { this.update(); } + private _name: string; private _currentIndex = -1; private _titles: Title[] = []; private _orientation: TabBar.Orientation; @@ -1209,6 +1223,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. * From a902e8c36c874037caca4692078a055b0b00ef76 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 16:50:43 -0700 Subject: [PATCH 07/14] Keep application-specific things out of phosphor. --- packages/widgets/src/dockpanel.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/widgets/src/dockpanel.ts b/packages/widgets/src/dockpanel.ts index 98a4031ee..593000d81 100644 --- a/packages/widgets/src/dockpanel.ts +++ b/packages/widgets/src/dockpanel.ts @@ -1299,7 +1299,7 @@ namespace DockPanel { * @returns A new tab bar for a dock panel. */ createTabBar(): TabBar { - let bar = new TabBar({name: `Activities ${this._tabBarCounter++}`}); + let bar = new TabBar(); bar.addClass('lm-DockPanel-tabBar'); /* */ bar.addClass('p-DockPanel-tabBar'); @@ -1320,7 +1320,6 @@ namespace DockPanel { /* */; return handle; } - private _tabBarCounter = 0; } /** From 0d70003e878a411830d53884868deef980d06579 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 23:49:17 -0700 Subject: [PATCH 08/14] Clean up tab panel adding widget ids and assuming tab bars are rendered. --- packages/widgets/src/tabpanel.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/widgets/src/tabpanel.ts b/packages/widgets/src/tabpanel.ts index e084b6ec5..127878125 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -34,7 +34,6 @@ import { import { Widget } from './widget'; -import { UUID } from '@phosphor/coreutils'; /** @@ -272,15 +271,12 @@ class TabPanel extends Widget { widget.hide(); } - widget.id = widget.id || `aria-${UUID.uuid4()}`; - this.stackedPanel.insertWidget(index, widget); this.tabBar.insertTab(index, widget.title); - let tab = this.tabBar.contentNode.children[this.tabBar.titles.indexOf(widget.title)]; - + let tabId = this.tabBar.renderer.createTabKey({title: widget.title, current: false, zIndex: 0}); widget.node.setAttribute('role', 'tabpanel'); - widget.node.setAttribute('aria-labelledby', tab.id); + widget.node.setAttribute('aria-labelledby', tabId); } /** @@ -342,9 +338,6 @@ class TabPanel extends Widget { private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void { widget.node.removeAttribute('role'); widget.node.removeAttribute('aria-labelledby'); - if (widget.id.slice(5) === 'aria-') { - widget.id = ''; - } this.tabBar.removeTab(widget.title); } From 99e4fc4dc672d1269d405e9bebcade762e8ac08a Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:36:42 -0700 Subject: [PATCH 09/14] Fix formatting and variable names for tabpanel --- packages/widgets/src/tabpanel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/widgets/src/tabpanel.ts b/packages/widgets/src/tabpanel.ts index 127878125..375de3699 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -270,7 +270,6 @@ class TabPanel extends Widget { if (widget !== this.currentWidget) { widget.hide(); } - this.stackedPanel.insertWidget(index, widget); this.tabBar.insertTab(index, widget.title); From 4605249eaab9f081e6000d50db8b06201566bcd7 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:44:07 -0700 Subject: [PATCH 10/14] Revert changes to default tab renderer and tab type parameters. Before, we needed to clamp down on these types for aria attributes. This is (a) backwards incompatible and (b) not needed anymore. --- packages/widgets/src/tabbar.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 6f5076df7..c4cbb91f9 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -53,7 +53,7 @@ import { * property should be set to `false` when rotating nodes from CSS. */ export -class TabBar extends Widget { +class TabBar extends Widget { /** * Construct a new tab bar. * @@ -1440,7 +1440,7 @@ namespace TabBar { * Subclasses are free to reimplement rendering methods as needed. */ export - class Renderer implements IRenderer { + class Renderer implements IRenderer { /** * Construct a new renderer. */ @@ -1458,7 +1458,7 @@ namespace TabBar { * * @returns A virtual element representing the tab. */ - renderTab(data: IRenderData): VirtualElement { + renderTab(data: IRenderData): VirtualElement { let title = data.title.caption; let key = this.createTabKey(data); let id = key; @@ -1482,7 +1482,7 @@ namespace TabBar { * * @returns A virtual element representing the tab icon. */ - renderIcon(data: IRenderData): VirtualElement { + renderIcon(data: IRenderData): VirtualElement { const { title } = data; let className = this.createIconClass(data); @@ -1503,7 +1503,7 @@ namespace TabBar { * * @returns A virtual element representing the tab label. */ - renderLabel(data: IRenderData): VirtualElement { + renderLabel(data: IRenderData): VirtualElement { return h.div({ className: 'lm-TabBar-tabLabel' /* */ @@ -1519,7 +1519,7 @@ namespace TabBar { * * @returns A virtual element representing the tab close icon. */ - renderCloseIcon(data: IRenderData): VirtualElement { + renderCloseIcon(data: IRenderData): VirtualElement { return h.div({ className: 'lm-TabBar-tabCloseIcon' /* */ @@ -1540,7 +1540,7 @@ namespace TabBar { * the key is generated. This enables efficient rendering of moved * tabs and avoids subtle hover style artifacts. */ - createTabKey(data: IRenderData): string { + createTabKey(data: IRenderData): string { let key = this._tabKeys.get(data.title); if (key === undefined) { key = `tab-key-${this._tabID++}`; @@ -1556,7 +1556,7 @@ namespace TabBar { * * @returns The inline style data for the tab. */ - createTabStyle(data: IRenderData): ElementInlineStyle { + createTabStyle(data: IRenderData): ElementInlineStyle { return { zIndex: `${data.zIndex}` }; } @@ -1567,7 +1567,7 @@ namespace TabBar { * * @returns The full class name for the tab. */ - createTabClass(data: IRenderData): string { + createTabClass(data: IRenderData): string { let name = 'lm-TabBar-tab'; /* */ name += ' p-TabBar-tab'; @@ -1597,7 +1597,7 @@ namespace TabBar { * * @returns The dataset for the tab. */ - createTabDataset(data: IRenderData): ElementDataset { + createTabDataset(data: IRenderData): ElementDataset { return data.title.dataset; } @@ -1608,7 +1608,7 @@ namespace TabBar { * * @returns The ARIA attributes for the tab. */ - createTabARIA(data: IRenderData): ElementARIAAttrs { + createTabARIA(data: IRenderData): ElementARIAAttrs { return {role: 'tab', 'aria-selected': data.current.toString()}; } @@ -1619,7 +1619,7 @@ namespace TabBar { * * @returns The full class name for the tab icon. */ - createIconClass(data: IRenderData): string { + createIconClass(data: IRenderData): string { let name = 'lm-TabBar-tabIcon'; /* */ name += ' p-TabBar-tabIcon'; @@ -1629,7 +1629,7 @@ namespace TabBar { } private _tabID = 0; - private _tabKeys = new WeakMap, string>(); + private _tabKeys = new WeakMap, string>(); } /** From d7ddc193078d6f000bc74221cfb5bcd31ae007ba Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:51:29 -0700 Subject: [PATCH 11/14] Add documentation for createTabKey. --- packages/widgets/src/tabbar.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index c4cbb91f9..952f5adcd 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -1430,6 +1430,17 @@ namespace TabBar { */ renderTab(data: IRenderData): VirtualElement; + /** + * Create a stable unique id for a tab based on the title. + * + * @param data - The data to use for the tab. + * + * @returns The unique id for a tab. + * + * #### Notes + * This method returns a stable unique id for a tab, depending only on the + * title. The tab DOM `id` is set to this value. + */ createTabKey(data: IRenderData): string; } From 8e68bcc798b1239e95362f3b6c93eb135f91a353 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:54:55 -0700 Subject: [PATCH 12/14] Always set the aria attributes for a tab panel. --- packages/widgets/src/docklayout.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/widgets/src/docklayout.ts b/packages/widgets/src/docklayout.ts index 48583fc0c..78deb386f 100644 --- a/packages/widgets/src/docklayout.ts +++ b/packages/widgets/src/docklayout.ts @@ -1996,11 +1996,8 @@ namespace Private { export async function addAria(widget: Widget, tabBar: TabBar) { let tabId = tabBar.renderer.createTabKey({title: widget.title, current: false, zIndex: 0}); - - if (tabId) { - widget.node.setAttribute('role', 'tabpanel'); - widget.node.setAttribute('aria-labelledby', tabId); - } + widget.node.setAttribute('role', 'tabpanel'); + widget.node.setAttribute('aria-labelledby', tabId); } export From 77a801762fee1c69a4c7709f19eb106e07139ab1 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 11:51:06 -0700 Subject: [PATCH 13/14] Add two TODO notes about where the tab aria-selected state might need to be updated. --- packages/widgets/src/tabbar.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 952f5adcd..4255459a1 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -1018,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)) { @@ -1071,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; From a6fdb77472d0999a95c9e6818e229dab409db51c Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 26 Jul 2019 12:11:46 -0700 Subject: [PATCH 14/14] Only create tab keys if we have a tab bar renderer. --- packages/widgets/src/docklayout.ts | 11 +++++++---- packages/widgets/src/tabbar.ts | 13 ------------- packages/widgets/src/tabpanel.ts | 8 ++++++-- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/widgets/src/docklayout.ts b/packages/widgets/src/docklayout.ts index 78deb386f..fca1d254c 100644 --- a/packages/widgets/src/docklayout.ts +++ b/packages/widgets/src/docklayout.ts @@ -1994,14 +1994,17 @@ namespace Private { } export - async function addAria(widget: Widget, tabBar: TabBar) { - let tabId = tabBar.renderer.createTabKey({title: widget.title, current: false, zIndex: 0}); + function addAria(widget: Widget, tabBar: TabBar) { widget.node.setAttribute('role', 'tabpanel'); - widget.node.setAttribute('aria-labelledby', tabId); + 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 - async function removeAria(widget: Widget) { + function removeAria(widget: Widget) { widget.node.removeAttribute('role'); widget.node.removeAttribute('aria-labelledby'); } diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 4255459a1..7cbe87c95 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -1434,19 +1434,6 @@ namespace TabBar { * @returns A virtual element representing the tab. */ renderTab(data: IRenderData): VirtualElement; - - /** - * Create a stable unique id for a tab based on the title. - * - * @param data - The data to use for the tab. - * - * @returns The unique id for a tab. - * - * #### Notes - * This method returns a stable unique id for a tab, depending only on the - * title. The tab DOM `id` is set to this value. - */ - createTabKey(data: IRenderData): string; } /** diff --git a/packages/widgets/src/tabpanel.ts b/packages/widgets/src/tabpanel.ts index 375de3699..7b324ca1e 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -273,9 +273,13 @@ class TabPanel extends Widget { this.stackedPanel.insertWidget(index, widget); this.tabBar.insertTab(index, widget.title); - let tabId = this.tabBar.renderer.createTabKey({title: widget.title, current: false, zIndex: 0}); widget.node.setAttribute('role', 'tabpanel'); - widget.node.setAttribute('aria-labelledby', tabId); + + 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); + } } /**