Skip to content
This repository has been archived by the owner on Nov 6, 2019. It is now read-only.

Add ARIA roles to tabs #406

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5df47af
Adds the menubar role.
zorkow May 15, 2019
e78d268
Adds initial set of aria roles to the menu.
zorkow May 15, 2019
390dea1
Refactors Aria Attributes into separate type.
zorkow May 15, 2019
eda2953
Adds Aria attributes to be treated special.
zorkow May 15, 2019
b034300
Adds roles and popup to all menu items. Corrects separator.
zorkow May 15, 2019
7de5cde
Add all ARIA attributes from the standard.
jasongrout May 16, 2019
b0cb6e8
Change to separator role.
zorkow May 16, 2019
660763c
Fix typo
jasongrout May 17, 2019
91e2adf
Fix formatting and variable names for menus
jasongrout May 17, 2019
547f73c
Add tab and tablist ARIA attributes for tabs.
jasongrout May 16, 2019
09e01c7
Add tab aria attributes in constructor.
jasongrout May 16, 2019
67860c6
Initial draft of adding tabpanel aria data for tabpanel and dockpanel.
jasongrout May 16, 2019
f6c56aa
Add aria-label and aria-selected to tab bars.
jasongrout May 16, 2019
0eba6f2
Remove aria-controls.
jasongrout May 16, 2019
cc3ef02
Add tab bar names, and default to “Activities <number>” for dockpanel.
jasongrout May 16, 2019
a73bdd3
Keep application-specific things out of phosphor.
jasongrout May 16, 2019
cef9b8a
Clean up tab panel adding widget ids and assuming tab bars are rendered.
jasongrout May 17, 2019
e193761
Fix formatting and variable names for tabpanel
jasongrout May 17, 2019
85951ae
Revert changes to default tab renderer and tab type parameters.
jasongrout May 17, 2019
272cc7e
Add documentation for createTabKey.
jasongrout May 17, 2019
b7914f7
Always set the aria attributes for a tab panel.
jasongrout May 17, 2019
4eab2dc
Add two TODO notes about where the tab aria-selected state might need…
jasongrout May 17, 2019
9121c4d
Only create tab keys if we have a tab bar renderer.
jasongrout Jul 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions packages/virtualdom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
);
Expand Down
22 changes: 22 additions & 0 deletions packages/widgets/src/docklayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -1976,6 +1981,22 @@ namespace Private {
}
}

export
function addAria(widget: Widget, tabBar: TabBar<Widget>) {
widget.node.setAttribute('role', 'tabpanel');
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
function removeAria(widget: Widget) {
widget.node.removeAttribute('role');
widget.node.removeAttribute('aria-labelledby');
}

/**
* Normalize a tab area config and collect the visited widgets.
*/
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 19 additions & 2 deletions packages/widgets/src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
} from '@phosphor/signaling';

import {
ElementDataset, VirtualDOM, VirtualElement, h
ARIAAttrNames, ElementARIAAttrs, ElementDataset, VirtualDOM, VirtualElement, h
} from '@phosphor/virtualdom';

import {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions packages/widgets/src/menubar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from '@phosphor/messaging';

import {
ElementDataset, VirtualDOM, VirtualElement, h
ElementARIAAttrs, ElementDataset, VirtualDOM, VirtualElement, h
} from '@phosphor/virtualdom';

import {
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down
55 changes: 51 additions & 4 deletions packages/widgets/src/tabbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
} from '@phosphor/signaling';

import {
ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h
ElementARIAAttrs, ElementDataset, ElementInlineStyle, VirtualDOM, VirtualElement, h
} from '@phosphor/virtualdom';

import {
Expand Down Expand Up @@ -60,14 +60,15 @@ class TabBar<T> extends Widget {
constructor(options: TabBar.IOptions<T> = {}) {
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;
}

/**
Expand Down Expand Up @@ -246,6 +247,25 @@ class TabBar<T> 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.
*
Expand Down Expand Up @@ -274,6 +294,7 @@ class TabBar<T> extends Widget {
// Toggle the orientation values.
this._orientation = value;
this.dataset['orientation'] = value;
this.contentNode.setAttribute('aria-orientation', value);
}

/**
Expand Down Expand Up @@ -893,6 +914,9 @@ class TabBar<T> extends Widget {
let ci = this._currentIndex;
let bh = this.insertBehavior;


// TODO: do we need to do an update to update the aria-selected attribute?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afshin points out that the two TODO items should be taken care of. Agreed. In a later version I just deleted the two notes, which may indicate that I thought about it and decided it wasn't necessary. That should be double-checked.


// 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)) {
Expand Down Expand Up @@ -946,6 +970,8 @@ class TabBar<T> 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;
Expand Down Expand Up @@ -1006,6 +1032,7 @@ class TabBar<T> extends Widget {
this.update();
}

private _name: string;
private _currentIndex = -1;
private _titles: Title<T>[] = [];
private _orientation: TabBar.Orientation;
Expand Down Expand Up @@ -1096,6 +1123,13 @@ namespace TabBar {
*/
export
interface IOptions<T> {
/**
* 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.
*
Expand Down Expand Up @@ -1318,11 +1352,13 @@ namespace TabBar {
renderTab(data: IRenderData<any>): 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 },
h.li({ id, key, className, title, style, dataset, ...aria },
this.renderIcon(data),
this.renderLabel(data),
this.renderCloseIcon(data)
Expand Down Expand Up @@ -1428,6 +1464,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<any>): ElementARIAAttrs {
return {role: 'tab', 'aria-selected': data.current.toString()};
}

/**
* Create the class name for the tab icon.
*
Expand Down
Loading