From 646204aff54c925a61feca8b7a046b4d8c167fc2 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 12:21:22 -0700 Subject: [PATCH 01/22] Adds the menubar role. --- packages/widgets/src/menubar.ts | 2 ++ packages/widgets/src/widget.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 457404794..f84e249a0 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -43,6 +43,7 @@ import { */ export class MenuBar extends Widget { + /** * Construct a new menu bar. * @@ -51,6 +52,7 @@ class MenuBar extends Widget { constructor(options: MenuBar.IOptions = {}) { super({ node: Private.createNode() }); this.addClass('p-MenuBar'); + this.addRole('menubar'); this.setFlag(Widget.Flag.DisallowLayout); this.renderer = options.renderer || MenuBar.defaultRenderer; } diff --git a/packages/widgets/src/widget.ts b/packages/widgets/src/widget.ts index 2325a1a64..bcc13beb0 100644 --- a/packages/widgets/src/widget.ts +++ b/packages/widgets/src/widget.ts @@ -338,6 +338,16 @@ class Widget implements IDisposable, IMessageHandler { return this.node.classList.toggle(name); } + /** + * Adds the aria role to the widget. + * + * @param role - The role to add. + */ + addRole(role: string): void { + this.node.setAttribute('role', role); + } + + /** * Post an `'update-request'` message to the widget. * From 2f8511d1b6514c9c41a746229d4b4221668b41d7 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 15:13:40 -0700 Subject: [PATCH 02/22] Adds initial set of aria roles to the menu. --- packages/virtualdom/src/index.ts | 3 +++ packages/widgets/src/menu.ts | 3 ++- packages/widgets/src/menubar.ts | 4 ++-- packages/widgets/src/widget.ts | 10 ---------- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index 43a5bb37b..d74ba5b70 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -25,6 +25,9 @@ import { */ export type ElementAttrNames = ( + 'role' | + 'aria-haspopup' | + 'abbr' | 'accept' | 'accept-charset' | diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index 910e8b5e2..d0934aba8 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -1144,7 +1144,7 @@ namespace Menu { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); return ( - h.li({ className, dataset }, + h.li({ className, dataset, role: 'menuitem' }, this.renderIcon(data), this.renderLabel(data), this.renderShortcut(data), @@ -1342,6 +1342,7 @@ namespace Private { let node = document.createElement('div'); let content = document.createElement('ul'); content.className = 'p-Menu-content'; + content.setAttribute('role', 'menu'); node.appendChild(content); node.tabIndex = -1; return node; diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index f84e249a0..e22082013 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -52,7 +52,6 @@ class MenuBar extends Widget { constructor(options: MenuBar.IOptions = {}) { super({ node: Private.createNode() }); this.addClass('p-MenuBar'); - this.addRole('menubar'); this.setFlag(Widget.Flag.DisallowLayout); this.renderer = options.renderer || MenuBar.defaultRenderer; } @@ -750,7 +749,7 @@ namespace MenuBar { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); return ( - h.li({ className, dataset }, + h.li({ className, dataset, role: 'menuitem', 'aria-haspopup': 'true' }, this.renderIcon(data), this.renderLabel(data) ) @@ -872,6 +871,7 @@ namespace Private { let node = document.createElement('div'); let content = document.createElement('ul'); content.className = 'p-MenuBar-content'; + content.setAttribute('role', 'menubar'); node.appendChild(content); node.tabIndex = -1; return node; diff --git a/packages/widgets/src/widget.ts b/packages/widgets/src/widget.ts index bcc13beb0..2325a1a64 100644 --- a/packages/widgets/src/widget.ts +++ b/packages/widgets/src/widget.ts @@ -338,16 +338,6 @@ class Widget implements IDisposable, IMessageHandler { return this.node.classList.toggle(name); } - /** - * Adds the aria role to the widget. - * - * @param role - The role to add. - */ - addRole(role: string): void { - this.node.setAttribute('role', role); - } - - /** * Post an `'update-request'` message to the widget. * From bd603a639fcadaf94c8757ae7f283041e7355b14 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 15:42:49 -0700 Subject: [PATCH 03/22] Refactors Aria Attributes into separate type. --- packages/virtualdom/src/index.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index d74ba5b70..d78fe3242 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -25,9 +25,6 @@ import { */ export type ElementAttrNames = ( - 'role' | - 'aria-haspopup' | - 'abbr' | 'accept' | 'accept-charset' | @@ -559,6 +556,13 @@ type ElementEventMap = { }; +export +type AriaAttrNames = ( + 'role' | + 'aria-haspopup' +); + + /** * An object which represents a dataset for a virtual DOM element. * @@ -655,6 +659,12 @@ type ElementSpecialAttrs = { }; +export +type ElementAriaAttrs = { + readonly [T in AriaAttrNames]?: string; +}; + + /** * The full set of attributes supported by a virtual element node. * @@ -664,6 +674,7 @@ type ElementSpecialAttrs = { export type ElementAttrs = ( ElementBaseAttrs & + ElementAriaAttrs & ElementEventAttrs & ElementSpecialAttrs ); From 9138f6d52ecf1b21e276c0914707189d960051f6 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 16:15:29 -0700 Subject: [PATCH 04/22] Adds Aria attributes to be treated special. --- packages/virtualdom/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index d78fe3242..5f3f8d286 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -656,6 +656,8 @@ type ElementSpecialAttrs = { * The inline style for the rendered DOM element. */ readonly style?: ElementInlineStyle; + + readonly aria?: ElementAriaAttrs; }; @@ -1175,6 +1177,7 @@ namespace Private { 'className': true, 'htmlFor': true, 'dataset': true, + 'aria': true, 'style': true, }; @@ -1213,6 +1216,15 @@ namespace Private { if (attrs.style) { addStyle(element, attrs.style); } + + // Add the aria attributes. + if (attrs.aria) { + for (let key in attrs.aria) { + element.setAttribute(key, (attrs.aria as any)[key]); + } + } + + } /** From e3d046a4176ba7cf90b318da35521bb7528d3267 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 16:40:45 -0700 Subject: [PATCH 05/22] Adds roles and popup to all menu items. Corrects separator. --- packages/widgets/src/menu.ts | 20 ++++++++++++++++++-- packages/widgets/src/menubar.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index d0934aba8..24350fb9b 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -34,7 +34,7 @@ import { } from '@phosphor/signaling'; import { - ElementDataset, VirtualDOM, VirtualElement, h + ElementAriaAttrs, ElementDataset, VirtualDOM, VirtualElement, h } from '@phosphor/virtualdom'; import { @@ -1143,8 +1143,9 @@ namespace Menu { renderItem(data: IRenderData): VirtualElement { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); + let aria = this.createItemAria(data); return ( - h.li({ className, dataset, role: 'menuitem' }, + h.li({ className, dataset, aria }, this.renderIcon(data), this.renderLabel(data), this.renderShortcut(data), @@ -1269,6 +1270,21 @@ namespace Menu { return extra ? `${name} ${extra}` : name; } + createItemAria(data: IRenderData): ElementAriaAttrs { + let aria: any = {}; + switch (data.item.type) { + case 'separator': + aria.role = 'separator'; + break; + case 'submenu': + aria['aria-haspopup'] = true; + break; + default: + aria.role = 'menuitem'; + } + return aria; + } + /** * Create the render content for the label node. * diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index e22082013..a37d1573a 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -22,7 +22,7 @@ import { } from '@phosphor/messaging'; import { - ElementDataset, VirtualDOM, VirtualElement, h + ElementAriaAttrs, ElementDataset, VirtualDOM, VirtualElement, h } from '@phosphor/virtualdom'; import { @@ -748,8 +748,9 @@ namespace MenuBar { renderItem(data: IRenderData): VirtualElement { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); + let aria = this.createItemAria(data); return ( - h.li({ className, dataset, role: 'menuitem', 'aria-haspopup': 'true' }, + h.li({ className, aria, dataset}, this.renderIcon(data), this.renderLabel(data) ) @@ -809,6 +810,10 @@ namespace MenuBar { return data.title.dataset; } + createItemAria(data: IRenderData): ElementAriaAttrs { + return {role: 'menuitem', 'aria-haspopup': 'true'}; + } + /** * Create the class name for the menu bar item icon. * From 7ee92fb1e81441340396e0c139e21a984eb41d23 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 18:03:16 -0700 Subject: [PATCH 06/22] Change to seperator role. --- packages/widgets/src/menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index 24350fb9b..77469a5ca 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -1274,7 +1274,7 @@ namespace Menu { let aria: any = {}; switch (data.item.type) { case 'separator': - aria.role = 'separator'; + aria.role = 'presentation'; break; case 'submenu': aria['aria-haspopup'] = true; From 780f296baddc9969126d5648263a4faa7be47616 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 20:05:49 -0700 Subject: [PATCH 07/22] Add all ARIA attributes from the standard. This commit also does some stylistic changes as well, like capitalizing ARIA and treating it like the other generic attributes. --- packages/virtualdom/src/index.ts | 105 ++++++++++++++++++++++--------- packages/widgets/src/menu.ts | 12 ++-- packages/widgets/src/menubar.ts | 8 +-- 3 files changed, 86 insertions(+), 39 deletions(-) diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index 5f3f8d286..3b0333643 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -124,6 +124,66 @@ type ElementAttrNames = ( ); +/** + * The names of ARIA attributes for HTML elements. + * + * The attribute names are collected from + * https://www.w3.org/TR/html5/infrastructure.html#element-attrdef-aria-role + */ +export +type ARIAAttrNames = ( + 'aria-activedescendant' | + 'aria-atomic' | + 'aria-autocomplete' | + 'aria-busy' | + 'aria-checked' | + 'aria-colcount' | + 'aria-colindex' | + 'aria-colspan' | + 'aria-controls' | + 'aria-current' | + 'aria-describedby' | + 'aria-details' | + 'aria-dialog' | + 'aria-disabled' | + 'aria-dropeffect' | + 'aria-errormessage' | + 'aria-expanded' | + 'aria-flowto' | + 'aria-grabbed' | + 'aria-haspopup' | + 'aria-hidden' | + 'aria-invalid' | + 'aria-keyshortcuts' | + 'aria-label' | + 'aria-labelledby' | + 'aria-level' | + 'aria-live' | + 'aria-multiline' | + 'aria-multiselectable' | + 'aria-orientation' | + 'aria-owns' | + 'aria-placeholder' | + 'aria-posinset' | + 'aria-pressed' | + 'aria-readonly' | + 'aria-relevant' | + 'aria-required' | + 'aria-roledescription' | + 'aria-rowcount' | + 'aria-rowindex' | + 'aria-rowspan' | + 'aria-selected' | + 'aria-setsize' | + 'aria-sort' | + 'aria-valuemax' | + 'aria-valuemin' | + 'aria-valuenow' | + 'aria-valuetext' | + 'role' +); + + /** * The names of the supported HTML5 CSS property names. * @@ -556,13 +616,6 @@ type ElementEventMap = { }; -export -type AriaAttrNames = ( - 'role' | - 'aria-haspopup' -); - - /** * An object which represents a dataset for a virtual DOM element. * @@ -595,7 +648,7 @@ type ElementInlineStyle = { * * These are the attributes which are applied to a real DOM element via * `element.setAttribute()`. The supported attribute names are defined - * by the `ElementAttrNames` type. + * by the `ElementAttrNames`type. * * Node attributes are specified using the lower-case HTML name instead * of the camel-case JS name due to browser inconsistencies in handling @@ -606,6 +659,18 @@ type ElementBaseAttrs = { readonly [T in ElementAttrNames]?: string; }; +/** + * The ARIA attributes for a virtual element node. + * + * These are the attributes which are applied to a real DOM element via + * `element.setAttribute()`. The supported attribute names are defined + * by the `ARIAAttrNames` type. + */ +export +type ElementARIAAttrs = { + readonly [T in ARIAAttrNames]?: string; +}; + /** * The inline event listener attributes for a virtual element node. @@ -656,27 +721,19 @@ type ElementSpecialAttrs = { * The inline style for the rendered DOM element. */ readonly style?: ElementInlineStyle; - - readonly aria?: ElementAriaAttrs; -}; - - -export -type ElementAriaAttrs = { - readonly [T in AriaAttrNames]?: string; }; /** * The full set of attributes supported by a virtual element node. * - * This is the combination of the base element attributes, the inline - * element event listeners, and the special element attributes. + * This is the combination of the base element attributes, the ARIA attributes, + * the inline element event listeners, and the special element attributes. */ export type ElementAttrs = ( ElementBaseAttrs & - ElementAriaAttrs & + ElementARIAAttrs & ElementEventAttrs & ElementSpecialAttrs ); @@ -1177,7 +1234,6 @@ namespace Private { 'className': true, 'htmlFor': true, 'dataset': true, - 'aria': true, 'style': true, }; @@ -1216,15 +1272,6 @@ namespace Private { if (attrs.style) { addStyle(element, attrs.style); } - - // Add the aria attributes. - if (attrs.aria) { - for (let key in attrs.aria) { - element.setAttribute(key, (attrs.aria as any)[key]); - } - } - - } /** diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index 24350fb9b..225696d85 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -34,7 +34,7 @@ import { } from '@phosphor/signaling'; import { - ElementAriaAttrs, ElementDataset, VirtualDOM, VirtualElement, h + ARIAAttrNames, ElementARIAAttrs, ElementDataset, VirtualDOM, VirtualElement, h } from '@phosphor/virtualdom'; import { @@ -1143,9 +1143,9 @@ namespace Menu { renderItem(data: IRenderData): VirtualElement { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); - let aria = this.createItemAria(data); + let attr = this.createItemARIA(data); return ( - h.li({ className, dataset, aria }, + h.li({ className, dataset, ...attr }, this.renderIcon(data), this.renderLabel(data), this.renderShortcut(data), @@ -1270,14 +1270,14 @@ namespace Menu { return extra ? `${name} ${extra}` : name; } - createItemAria(data: IRenderData): ElementAriaAttrs { - let aria: any = {}; + createItemARIA(data: IRenderData): ElementARIAAttrs { + let aria: {[T in ARIAAttrNames]?: string} = {}; switch (data.item.type) { case 'separator': aria.role = 'separator'; break; case 'submenu': - aria['aria-haspopup'] = true; + aria['aria-haspopup'] = 'true'; break; default: aria.role = 'menuitem'; diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index a37d1573a..d48afaa2c 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -22,7 +22,7 @@ import { } from '@phosphor/messaging'; import { - ElementAriaAttrs, ElementDataset, VirtualDOM, VirtualElement, h + ElementARIAAttrs, ElementDataset, VirtualDOM, VirtualElement, h } from '@phosphor/virtualdom'; import { @@ -748,9 +748,9 @@ namespace MenuBar { renderItem(data: IRenderData): VirtualElement { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); - let aria = this.createItemAria(data); + let aria = this.createItemARIA(data); return ( - h.li({ className, aria, dataset}, + h.li({ className, dataset, ...aria}, this.renderIcon(data), this.renderLabel(data) ) @@ -810,7 +810,7 @@ namespace MenuBar { return data.title.dataset; } - createItemAria(data: IRenderData): ElementAriaAttrs { + createItemARIA(data: IRenderData): ElementARIAAttrs { return {role: 'menuitem', 'aria-haspopup': 'true'}; } From 94b7612a0a035f5b736979103327f32b858845c7 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 20:21:46 -0700 Subject: [PATCH 08/22] 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 a50c00a7d..cae9c52ed 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -30,7 +30,7 @@ import { } from '@phosphor/signaling'; import { - ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h + ElementARIAAttrs, ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h } from '@phosphor/virtualdom'; import { @@ -1321,8 +1321,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) @@ -1428,6 +1429,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. * @@ -1587,6 +1599,7 @@ namespace Private { function createNode(): HTMLDivElement { let node = document.createElement('div'); let content = document.createElement('ul'); + content.setAttribute('role', 'tablist'); content.className = 'p-TabBar-content'; node.appendChild(content); return node; From 83cad0f3b5a463e54d512130e09871107ec3ed84 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 21:16:40 -0700 Subject: [PATCH 09/22] Add tab aria attributes in constructor. --- packages/widgets/src/tabbar.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index cae9c52ed..2d0d23786 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -68,6 +68,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); } /** @@ -1599,7 +1605,6 @@ namespace Private { function createNode(): HTMLDivElement { let node = document.createElement('div'); let content = document.createElement('ul'); - content.setAttribute('role', 'tablist'); content.className = 'p-TabBar-content'; node.appendChild(content); return node; From 22ae36432d9920ff349597a1b581e7ffdf90f50b Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 03:02:40 -0700 Subject: [PATCH 10/22] 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 b22c3b20a..edd7ed445 100644 --- a/packages/widgets/src/docklayout.ts +++ b/packages/widgets/src/docklayout.ts @@ -633,6 +633,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); @@ -764,6 +766,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; } @@ -789,6 +792,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); } /** @@ -809,6 +813,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) { @@ -1976,6 +1981,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. */ @@ -2065,6 +2086,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 2d0d23786..bca36b7c1 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -51,7 +51,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. * @@ -280,6 +280,7 @@ class TabBar extends Widget { // Toggle the orientation values. this._orientation = value; this.dataset['orientation'] = value; + this.contentNode.setAttribute('aria-orientation', value); } /** @@ -1294,6 +1295,8 @@ namespace TabBar { * @returns A virtual element representing the tab. */ renderTab(data: IRenderData): VirtualElement; + + createTabKey(data: IRenderData): string; } /** @@ -1303,7 +1306,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. */ @@ -1321,15 +1324,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) @@ -1344,7 +1348,7 @@ namespace TabBar { * * @returns A virtual element representing the tab icon. */ - renderIcon(data: IRenderData): VirtualElement { + renderIcon(data: IRenderData): VirtualElement { let className = this.createIconClass(data); return h.div({ className }, data.title.iconLabel); } @@ -1356,7 +1360,7 @@ namespace TabBar { * * @returns A virtual element representing the tab label. */ - renderLabel(data: IRenderData): VirtualElement { + renderLabel(data: IRenderData): VirtualElement { return h.div({ className: 'p-TabBar-tabLabel' }, data.title.label); } @@ -1367,7 +1371,7 @@ namespace TabBar { * * @returns A virtual element representing the tab close icon. */ - renderCloseIcon(data: IRenderData): VirtualElement { + renderCloseIcon(data: IRenderData): VirtualElement { return h.div({ className: 'p-TabBar-tabCloseIcon' }); } @@ -1383,7 +1387,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++}`; @@ -1399,7 +1403,7 @@ namespace TabBar { * * @returns The inline style data for the tab. */ - createTabStyle(data: IRenderData): ElementInlineStyle { + createTabStyle(data: IRenderData): ElementInlineStyle { return { zIndex: `${data.zIndex}` }; } @@ -1410,7 +1414,7 @@ namespace TabBar { * * @returns The full class name for the tab. */ - createTabClass(data: IRenderData): string { + createTabClass(data: IRenderData): string { let name = 'p-TabBar-tab'; if (data.title.className) { name += ` ${data.title.className}`; @@ -1431,7 +1435,7 @@ namespace TabBar { * * @returns The dataset for the tab. */ - createTabDataset(data: IRenderData): ElementDataset { + createTabDataset(data: IRenderData): ElementDataset { return data.title.dataset; } @@ -1442,8 +1446,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}; } /** @@ -1453,14 +1457,14 @@ namespace TabBar { * * @returns The full class name for the tab icon. */ - createIconClass(data: IRenderData): string { + createIconClass(data: IRenderData): string { let name = 'p-TabBar-tabIcon'; let extra = data.title.iconClass; return extra ? `${name} ${extra}` : name; } 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 7d7c91deb..f3149d157 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -32,6 +32,7 @@ import { import { Widget } from './widget'; +import { UUID } from '@phosphor/coreutils'; /** @@ -261,8 +262,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); } /** @@ -322,6 +331,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 bc3c00921f6a7305aa59b940a2de71334bc8d802 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 03:53:05 -0700 Subject: [PATCH 11/22] 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 bca36b7c1..efc619522 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -74,6 +74,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'); } /** @@ -1447,7 +1450,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 50ee3c37925bf5f7f0c1b4bc32bfbeb1ddf8f15b Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 14:08:35 -0700 Subject: [PATCH 12/22] 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 efc619522..83d3c8d6b 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -69,8 +69,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); @@ -1450,7 +1448,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 072106212f36e6d7e899e1ec1520a9388f103753 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 16:36:54 -0700 Subject: [PATCH 13/22] =?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 c2a0761fc..005fd0665 100644 --- a/packages/widgets/src/dockpanel.ts +++ b/packages/widgets/src/dockpanel.ts @@ -1193,7 +1193,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('p-DockPanel-tabBar'); return bar; } @@ -1208,6 +1208,7 @@ namespace DockPanel { handle.className = 'p-DockPanel-handle'; return handle; } + private _tabBarCounter = 0; } /** diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 83d3c8d6b..fc55661fb 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -60,21 +60,15 @@ class TabBar extends Widget { constructor(options: TabBar.IOptions = {}) { super({ node: Private.createNode() }); this.addClass('p-TabBar'); + this.contentNode.setAttribute('role', 'tablist'); this.setFlag(Widget.Flag.DisallowLayout); this.tabsMovable = options.tabsMovable || 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'); } /** @@ -253,6 +247,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. * @@ -1014,6 +1027,7 @@ class TabBar extends Widget { this.update(); } + private _name: string; private _currentIndex = -1; private _titles: Title[] = []; private _orientation: TabBar.Orientation; @@ -1104,6 +1118,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 7f0cb215a3123c7e9f7dae5b07954743acbf7fdb Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 16:50:43 -0700 Subject: [PATCH 14/22] 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 005fd0665..c2a0761fc 100644 --- a/packages/widgets/src/dockpanel.ts +++ b/packages/widgets/src/dockpanel.ts @@ -1193,7 +1193,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('p-DockPanel-tabBar'); return bar; } @@ -1208,7 +1208,6 @@ namespace DockPanel { handle.className = 'p-DockPanel-handle'; return handle; } - private _tabBarCounter = 0; } /** From 9761aeef7fe355fcc6f7911edb17d2448fd84def Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 23:49:17 -0700 Subject: [PATCH 15/22] 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 f3149d157..2d932c58f 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -32,7 +32,6 @@ import { import { Widget } from './widget'; -import { UUID } from '@phosphor/coreutils'; /** @@ -263,15 +262,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); } /** @@ -333,9 +329,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 eb3b6a23707f88322ea4f084695ca307a8f34b1d Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:26:42 -0700 Subject: [PATCH 16/22] Add isToggleable command state This allows interfaces (including accessible ones) to indicate to the user that the command may toggle state. --- packages/commands/src/index.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 5e356be58..fbf08046a 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -313,6 +313,21 @@ class CommandRegistry { return cmd ? cmd.isToggled.call(undefined, args) : false; } + /** + * Test whether a specific command is toggleable. + * + * @param id - The id of the command of interest. + * + * @param args - The arguments for the command. + * + * @returns A boolean indicating whether the command is toggleable, + * or `false` if the command is not registered. + */ + isToggleable(id: string, args: ReadonlyJSONObject = JSONExt.emptyObject): boolean { + let cmd = this._commands[id]; + return cmd ? cmd.isToggleable : false; + } + /** * Test whether a specific command is visible. * @@ -759,6 +774,20 @@ namespace CommandRegistry { */ isToggled?: CommandFunc; + /** + * A function which indicates whether the command is toggleable. + * + * #### Notes + * Visual representations may use this value to display a toggled command in + * a different form, such as a check box for a menu item or a depressed + * state for a toggle button. This attribute also allows for accessible + * interfaces to notify the user that the command corresponds to some state. + * + * The default value is `true` if an `isToggled` function is given, `false` + * otherwise. + */ + isToggleable?: boolean; + /** * A function which indicates whether the command is visible. * @@ -1147,6 +1176,7 @@ namespace Private { readonly dataset: CommandFunc; readonly isEnabled: CommandFunc; readonly isToggled: CommandFunc; + readonly isToggleable: boolean; readonly isVisible: CommandFunc; } @@ -1167,6 +1197,7 @@ namespace Private { dataset: asFunc(options.dataset, emptyDatasetFunc), isEnabled: options.isEnabled || trueFunc, isToggled: options.isToggled || falseFunc, + isToggleable: options.isToggleable || !!options.isToggled, isVisible: options.isVisible || trueFunc }; } From fe9eabf925b5e20ed9769e8f837795e2af401cda Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:32:56 -0700 Subject: [PATCH 17/22] Fix typo --- packages/virtualdom/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index 3b0333643..417b2eb2d 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -648,7 +648,7 @@ type ElementInlineStyle = { * * These are the attributes which are applied to a real DOM element via * `element.setAttribute()`. The supported attribute names are defined - * by the `ElementAttrNames`type. + * by the `ElementAttrNames` type. * * Node attributes are specified using the lower-case HTML name instead * of the camel-case JS name due to browser inconsistencies in handling From 953f2f0e8f3ab16209bb0f6fb78b96707ae40a2b Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:36:42 -0700 Subject: [PATCH 18/22] Fix formatting and variable names. --- packages/widgets/src/menu.ts | 4 ++-- packages/widgets/src/menubar.ts | 1 - packages/widgets/src/tabpanel.ts | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index 4f511006e..a966d1a52 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -1143,9 +1143,9 @@ namespace Menu { renderItem(data: IRenderData): VirtualElement { let className = this.createItemClass(data); let dataset = this.createItemDataset(data); - let attr = this.createItemARIA(data); + let aria = this.createItemARIA(data); return ( - h.li({ className, dataset, ...attr }, + h.li({ className, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data), this.renderShortcut(data), diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index d48afaa2c..8b2a4f1ae 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -43,7 +43,6 @@ import { */ export class MenuBar extends Widget { - /** * Construct a new menu bar. * diff --git a/packages/widgets/src/tabpanel.ts b/packages/widgets/src/tabpanel.ts index 2d932c58f..9d389aca6 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -261,7 +261,6 @@ class TabPanel extends Widget { if (widget !== this.currentWidget) { widget.hide(); } - this.stackedPanel.insertWidget(index, widget); this.tabBar.insertTab(index, widget.title); From 8abf54a89739bdb6e08216c9593f386ffc2cac3c Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:44:07 -0700 Subject: [PATCH 19/22] 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 fc55661fb..b0f1523f8 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -51,7 +51,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. * @@ -1328,7 +1328,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. */ @@ -1346,7 +1346,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; @@ -1370,7 +1370,7 @@ namespace TabBar { * * @returns A virtual element representing the tab icon. */ - renderIcon(data: IRenderData): VirtualElement { + renderIcon(data: IRenderData): VirtualElement { let className = this.createIconClass(data); return h.div({ className }, data.title.iconLabel); } @@ -1382,7 +1382,7 @@ namespace TabBar { * * @returns A virtual element representing the tab label. */ - renderLabel(data: IRenderData): VirtualElement { + renderLabel(data: IRenderData): VirtualElement { return h.div({ className: 'p-TabBar-tabLabel' }, data.title.label); } @@ -1393,7 +1393,7 @@ namespace TabBar { * * @returns A virtual element representing the tab close icon. */ - renderCloseIcon(data: IRenderData): VirtualElement { + renderCloseIcon(data: IRenderData): VirtualElement { return h.div({ className: 'p-TabBar-tabCloseIcon' }); } @@ -1409,7 +1409,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++}`; @@ -1425,7 +1425,7 @@ namespace TabBar { * * @returns The inline style data for the tab. */ - createTabStyle(data: IRenderData): ElementInlineStyle { + createTabStyle(data: IRenderData): ElementInlineStyle { return { zIndex: `${data.zIndex}` }; } @@ -1436,7 +1436,7 @@ namespace TabBar { * * @returns The full class name for the tab. */ - createTabClass(data: IRenderData): string { + createTabClass(data: IRenderData): string { let name = 'p-TabBar-tab'; if (data.title.className) { name += ` ${data.title.className}`; @@ -1457,7 +1457,7 @@ namespace TabBar { * * @returns The dataset for the tab. */ - createTabDataset(data: IRenderData): ElementDataset { + createTabDataset(data: IRenderData): ElementDataset { return data.title.dataset; } @@ -1468,7 +1468,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()}; } @@ -1479,14 +1479,14 @@ namespace TabBar { * * @returns The full class name for the tab icon. */ - createIconClass(data: IRenderData): string { + createIconClass(data: IRenderData): string { let name = 'p-TabBar-tabIcon'; let extra = data.title.iconClass; return extra ? `${name} ${extra}` : name; } private _tabID = 0; - private _tabKeys = new WeakMap, string>(); + private _tabKeys = new WeakMap, string>(); } /** From 5a6b022e6d26a802b51a4ad6c22e9187035a0a1c Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:51:29 -0700 Subject: [PATCH 20/22] 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 b0f1523f8..dfe600d6c 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -1318,6 +1318,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 c6cae4a322b97830c80e3101a70f9f9a303fa717 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:54:55 -0700 Subject: [PATCH 21/22] 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 edd7ed445..185de2ff7 100644 --- a/packages/widgets/src/docklayout.ts +++ b/packages/widgets/src/docklayout.ts @@ -1984,11 +1984,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 fd4f2b1a15012b63dce18a4ecbe389e9ea8f24ac Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 11:51:06 -0700 Subject: [PATCH 22/22] 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 dfe600d6c..e3892cacb 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -914,6 +914,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)) { @@ -967,6 +970,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;