From 5df47af943380110feb871a42dd5e0842e5ebf61 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 12:21:22 -0700 Subject: [PATCH 01/23] 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 2fd0985d6..0db4d002c 100644 --- a/packages/widgets/src/widget.ts +++ b/packages/widgets/src/widget.ts @@ -338,6 +338,16 @@ class Widget implements IMessageHandler, IObservableDisposable { 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 e78d2683629ab1551c945ac924fc38746044ba7d Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 15:13:40 -0700 Subject: [PATCH 02/23] 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 0db4d002c..2fd0985d6 100644 --- a/packages/widgets/src/widget.ts +++ b/packages/widgets/src/widget.ts @@ -338,16 +338,6 @@ class Widget implements IMessageHandler, IObservableDisposable { 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 390dea1f71a3e1df4454970ea3d663911fbedb64 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 15:42:49 -0700 Subject: [PATCH 03/23] 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 eda29536ee8f2c9ab3c704dbc470d2df75ca178e Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 16:15:29 -0700 Subject: [PATCH 04/23] 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 b0343008b188af5a1494cb06d04f057e957e37fe Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 16:40:45 -0700 Subject: [PATCH 05/23] 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 7de5cde386ef3c132fea2fa153ddaf992c02b482 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 20:05:49 -0700 Subject: [PATCH 06/23] 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 b0cb6e8020722c29348ab09d612cb99db8c414d1 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 15 May 2019 18:03:16 -0700 Subject: [PATCH 07/23] Change to separator 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 225696d85..4f511006e 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -1274,7 +1274,7 @@ namespace Menu { let aria: {[T in ARIAAttrNames]?: string} = {}; switch (data.item.type) { case 'separator': - aria.role = 'separator'; + aria.role = 'presentation'; break; case 'submenu': aria['aria-haspopup'] = 'true'; From 660763cd6fd14b9bfeecad07bf1c5dfc0e45e2db Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:32:56 -0700 Subject: [PATCH 08/23] 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 91e2adf73f7a37389399d04c7b59585fd84f7a41 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:36:42 -0700 Subject: [PATCH 09/23] Fix formatting and variable names for menus --- packages/widgets/src/menu.ts | 4 ++-- packages/widgets/src/menubar.ts | 1 - 2 files changed, 2 insertions(+), 3 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. * From 547f73cde10fc123d6a7fc820820fae4a0f2ab13 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 20:21:46 -0700 Subject: [PATCH 10/23] 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 09e01c76901cf0e57067134c9bea70a56307dfc5 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 15 May 2019 21:16:40 -0700 Subject: [PATCH 11/23] 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 67860c6b9e620ceb1b8befc44a8366796ccfb6c8 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 03:02:40 -0700 Subject: [PATCH 12/23] 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 f6c56aa34495f7c99748f322f102d9c5924a6716 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 03:53:05 -0700 Subject: [PATCH 13/23] 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 0eba6f2be308fb2e023f3134d8d2418f6385b357 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 14:08:35 -0700 Subject: [PATCH 14/23] 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 cc3ef02dbc3ddb866dd46da15f92f524fdb498dc Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 16:36:54 -0700 Subject: [PATCH 15/23] =?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 c9fc523cc..72bbd1fc7 100644 --- a/packages/widgets/src/dockpanel.ts +++ b/packages/widgets/src/dockpanel.ts @@ -1246,7 +1246,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; } @@ -1261,6 +1261,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 a73bdd3e82a1e2ba6a3e302ff8226fb09e6d97ee Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 16:50:43 -0700 Subject: [PATCH 16/23] 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 72bbd1fc7..c9fc523cc 100644 --- a/packages/widgets/src/dockpanel.ts +++ b/packages/widgets/src/dockpanel.ts @@ -1246,7 +1246,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; } @@ -1261,7 +1261,6 @@ namespace DockPanel { handle.className = 'p-DockPanel-handle'; return handle; } - private _tabBarCounter = 0; } /** From cef9b8a994cd3adccf8acdd8b03e34111f5027a6 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 16 May 2019 23:49:17 -0700 Subject: [PATCH 17/23] 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 e193761cb59307a05298a3f564f473a124eacc44 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:36:42 -0700 Subject: [PATCH 18/23] 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 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 85951ae6d57247fb8845179100cdafc9845b8520 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:44:07 -0700 Subject: [PATCH 19/23] 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 272cc7ea3173d487684d513c0faa2aef763f8197 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:51:29 -0700 Subject: [PATCH 20/23] 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 b7914f7b23da245f19ad0595b46c3e680137a560 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 10:54:55 -0700 Subject: [PATCH 21/23] 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 4eab2dca707b8ecdcaf5b21e9bde2b44ca572944 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 17 May 2019 11:51:06 -0700 Subject: [PATCH 22/23] 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; From 9121c4deaef864f880c3e75efe4b2baa62feb85a Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 26 Jul 2019 12:11:46 -0700 Subject: [PATCH 23/23] 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 185de2ff7..857935286 100644 --- a/packages/widgets/src/docklayout.ts +++ b/packages/widgets/src/docklayout.ts @@ -1982,14 +1982,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 e3892cacb..46cea33e0 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -1322,19 +1322,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 9d389aca6..e4a1a0b93 100644 --- a/packages/widgets/src/tabpanel.ts +++ b/packages/widgets/src/tabpanel.ts @@ -264,9 +264,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); + } } /**