From f999b9c54006ba2934786a529bc78ff7490f9799 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Wed, 11 Nov 2020 14:18:03 -0600 Subject: [PATCH 1/2] Add aria roles to menus --- packages/virtualdom/src/index.ts | 79 +++++++++++++++++++++++++++++++- packages/widgets/src/menu.ts | 21 ++++++++- packages/widgets/src/menubar.ts | 10 +++- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index 21f0887dc..81135fdac 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -126,6 +126,67 @@ 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. * @@ -585,6 +646,19 @@ type ElementInlineStyle = { }; +/** + * 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 base attributes for a virtual element node. * @@ -657,12 +731,13 @@ type ElementSpecialAttrs = { /** * 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 the ARIA attributes, + * the inline element event listeners, and the special element attributes. */ export type ElementAttrs = ( ElementBaseAttrs & + ElementARIAAttrs & ElementEventAttrs & ElementSpecialAttrs ); diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index 49c3d19d1..88b3a3634 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -36,7 +36,7 @@ import { } from '@lumino/signaling'; import { - ElementDataset, VirtualDOM, VirtualElement, h + ARIAAttrNames, ElementARIAAttrs, ElementDataset, VirtualDOM, VirtualElement, h } from '@lumino/virtualdom'; import { @@ -1149,8 +1149,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 }, + h.li({ className, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data), this.renderShortcut(data), @@ -1319,6 +1320,21 @@ namespace Menu { return extra ? `${name} ${extra}` : name; } + createItemARIA(data: IRenderData): ElementARIAAttrs { + let aria: {[T in ARIAAttrNames]?: string} = {}; + switch (data.item.type) { + case 'separator': + aria.role = 'presentation'; + break; + case 'submenu': + aria['aria-haspopup'] = 'true'; + break; + default: + aria.role = 'menuitem'; + } + return aria; + } + /** * Create the render content for the label node. * @@ -1401,6 +1417,7 @@ namespace Private { content.classList.add('p-Menu-content'); /* */ node.appendChild(content); + content.setAttribute('role', 'menu'); node.tabIndex = -1; return node; } diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 88a8cffc2..61934a4c4 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -24,7 +24,7 @@ import { } from '@lumino/messaging'; import { - ElementDataset, VirtualDOM, VirtualElement, h + ElementARIAAttrs, ElementDataset, VirtualDOM, VirtualElement, h } from '@lumino/virtualdom'; import { @@ -770,8 +770,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 }, + h.li({ className, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data) ) @@ -850,6 +851,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. * @@ -924,6 +929,7 @@ namespace Private { content.classList.add('p-MenuBar-content'); /* */ node.appendChild(content); + content.setAttribute('role', 'menubar'); node.tabIndex = -1; return node; } From a84632db2ce6b9354d7908bb5cf6e24960682a8d Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Fri, 13 Nov 2020 11:52:32 -0500 Subject: [PATCH 2/2] Add docstring to createItemAria --- packages/widgets/src/menu.ts | 9 ++++++++- packages/widgets/src/menubar.ts | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/widgets/src/menu.ts b/packages/widgets/src/menu.ts index 88b3a3634..76d95f1e2 100644 --- a/packages/widgets/src/menu.ts +++ b/packages/widgets/src/menu.ts @@ -1319,7 +1319,14 @@ namespace Menu { let extra = data.item.iconClass; return extra ? `${name} ${extra}` : name; } - + + /** + * Create the aria attributes for menu item. + * + * @param data - The data to use for the aria attributes. + * + * @returns The aria attributes object for the item. + */ createItemARIA(data: IRenderData): ElementARIAAttrs { let aria: {[T in ARIAAttrNames]?: string} = {}; switch (data.item.type) { diff --git a/packages/widgets/src/menubar.ts b/packages/widgets/src/menubar.ts index 61934a4c4..e16b99ae1 100644 --- a/packages/widgets/src/menubar.ts +++ b/packages/widgets/src/menubar.ts @@ -851,6 +851,13 @@ namespace MenuBar { return data.title.dataset; } + /** + * Create the aria attributes for menu bar item. + * + * @param data - The data to use for the aria attributes. + * + * @returns The aria attributes object for the item. + */ createItemARIA(data: IRenderData): ElementARIAAttrs { return {role: 'menuitem', 'aria-haspopup': 'true'}; }