diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index 43a5bb37b..417b2eb2d 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. * @@ -599,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. @@ -655,12 +727,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 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 910e8b5e2..a966d1a52 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 + ARIAAttrNames, 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 }, + 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: {[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. * @@ -1342,6 +1358,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 457404794..8b2a4f1ae 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 { @@ -747,8 +747,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) ) @@ -808,6 +809,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. * @@ -870,6 +875,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;