Skip to content

Commit

Permalink
[added] Accessibility: use appropriate ARIA's when an id is given to …
Browse files Browse the repository at this point in the history
…the tabbed area
  • Loading branch information
jquense committed Jun 18, 2015
1 parent 8752754 commit 1e552cc
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 5 deletions.
6 changes: 5 additions & 1 deletion src/TabPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ const TabPane = React.createClass({
};

return (
<div {...this.props} className={classNames(this.props.className, classes)}>
<div {...this.props}
role='tabpanel'
aria-hidden={!this.props.active}
className={classNames(this.props.className, classes)}
>
{this.props.children}
</div>
);
Expand Down
20 changes: 16 additions & 4 deletions src/TabbedArea.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import ValidComponentChildren from './utils/ValidComponentChildren';
import Nav from './Nav';
import NavItem from './NavItem';

let panelId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___panel___' + child.props.eventKey);
let tabId = (props, child) => child.props.id ? child.props.id + '___tab' : props.id && (props.id + '___tab___' + child.props.eventKey);

function getDefaultActiveKeyFromChildren(children) {
let defaultActiveKey;

Expand Down Expand Up @@ -61,6 +64,8 @@ const TabbedArea = React.createClass({
},

render() {
let { id, ...props } = this.props; // eslint-disable-line object-shorthand

let activeKey =
this.props.activeKey != null ? this.props.activeKey : this.state.activeKey;

Expand All @@ -69,15 +74,15 @@ const TabbedArea = React.createClass({
}

let nav = (
<Nav {...this.props} activeKey={activeKey} onSelect={this.handleSelect} ref="tabs">
<Nav {...props} activeKey={activeKey} onSelect={this.handleSelect} ref="tabs">
{ValidComponentChildren.map(this.props.children, renderTabIfSet, this)}
</Nav>
);

return (
<div>
{nav}
<div id={this.props.id} className="tab-content" ref="panes">
<div id={id} className="tab-content" ref="panes">
{ValidComponentChildren.map(this.props.children, this.renderPane)}
</div>
</div>
Expand All @@ -91,11 +96,15 @@ const TabbedArea = React.createClass({
renderPane(child, index) {
let activeKey = this.getActiveKey();

let active = (child.props.eventKey === activeKey &&
(this.state.previousActiveKey == null || !this.props.animation));

return cloneElement(
child,
{
active: (child.props.eventKey === activeKey &&
(this.state.previousActiveKey == null || !this.props.animation)),
active,
id: panelId(this.props, child),
'aria-labelledby': tabId(this.props, child),
key: child.key ? child.key : index,
animation: this.props.animation,
onAnimateOutEnd: (this.state.previousActiveKey != null &&
Expand All @@ -106,9 +115,12 @@ const TabbedArea = React.createClass({

renderTab(child) {
let {eventKey, className, tab, disabled } = child.props;

return (
<NavItem
linkId={tabId(this.props, child)}
ref={'tab' + eventKey}
aria-controls={panelId(this.props, child)}
eventKey={eventKey}
className={className}
disabled={disabled}>
Expand Down
19 changes: 19 additions & 0 deletions test/TabPaneSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,23 @@ describe('TabPane', function () {
);
assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active'));
});

describe('Web Accessibility', function(){

it('Should have aria-hidden', function () {
let instance = ReactTestUtils.renderIntoDocument(
<TabPane active>Item content</TabPane>
);

assert.equal(React.findDOMNode(instance).getAttribute('aria-hidden'), 'false');
});

it('Should have role', function () {
let instance = ReactTestUtils.renderIntoDocument(
<TabPane active>Item content</TabPane>
);

assert.equal(React.findDOMNode(instance).getAttribute('role'), 'tabpanel');
});
});
});
45 changes: 45 additions & 0 deletions test/TabbedAreaSpec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import ReactTestUtils from 'react/lib/ReactTestUtils';
import TabbedArea from '../src/TabbedArea';
import NavItem from '../src/NavItem';
import TabPane from '../src/TabPane';
import ValidComponentChildren from '../src/utils/ValidComponentChildren';

Expand Down Expand Up @@ -229,5 +230,49 @@ describe('TabbedArea', function () {
assert.equal(tabbedArea.refs.tabs.props.activeKey, 2);
});

describe('Web Accessibility', function(){

it('Should generate ids from parent id', function () {
let instance = ReactTestUtils.renderIntoDocument(
<TabbedArea defaultActiveKey={2} id='tabs'>
<TabPane tab="Tab 1" eventKey={1}>Tab 1 content</TabPane>
<TabPane tab="Tab 2" eventKey={2}>Tab 2 content</TabPane>
</TabbedArea>
);

let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem);

tabs.every(tab =>
assert.ok(tab.props['aria-controls'] && tab.props.linkId));
});

it('Should add aria-controls', function () {
let instance = ReactTestUtils.renderIntoDocument(
<TabbedArea defaultActiveKey={2} id='tabs'>
<TabPane id='pane-1' tab="Tab 1" eventKey={1}>Tab 1 content</TabPane>
<TabPane id='pane-2' tab="Tab 2" eventKey={2}>Tab 2 content</TabPane>
</TabbedArea>
);

let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane);

assert.equal(panes[0].props['aria-labelledby'], 'pane-1___tab');
assert.equal(panes[1].props['aria-labelledby'], 'pane-2___tab');
});

it('Should add aria-controls', function () {
let instance = ReactTestUtils.renderIntoDocument(
<TabbedArea defaultActiveKey={2} id='tabs'>
<TabPane id='pane-1' tab="Tab 1" eventKey={1}>Tab 1 content</TabPane>
<TabPane id='pane-2' tab="Tab 2" eventKey={2}>Tab 2 content</TabPane>
</TabbedArea>
);

let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem);

assert.equal(tabs[0].props['aria-controls'], 'pane-1');
assert.equal(tabs[1].props['aria-controls'], 'pane-2');
});

});
});

0 comments on commit 1e552cc

Please sign in to comment.