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..76d95f1e2 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), @@ -1318,6 +1319,28 @@ 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) { + 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 +1424,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..e16b99ae1 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,17 @@ 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'}; + } + /** * Create the class name for the menu bar item icon. * @@ -924,6 +936,7 @@ namespace Private { content.classList.add('p-MenuBar-content'); /* */ node.appendChild(content); + content.setAttribute('role', 'menubar'); node.tabIndex = -1; return node; }