Skip to content

Commit

Permalink
Merge pull request react-bootstrap#857 from react-bootstrap/accessibi…
Browse files Browse the repository at this point in the history
…lity

Accessibility
  • Loading branch information
mtscout6 committed Jun 20, 2015
2 parents 3110e0b + ccc50e0 commit 903b5d1
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 37 deletions.
14 changes: 8 additions & 6 deletions src/Alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ const Alert = React.createClass({

propTypes: {
onDismiss: React.PropTypes.func,
dismissAfter: React.PropTypes.number
dismissAfter: React.PropTypes.number,
closeLabel: React.PropTypes.string
},

getDefaultProps() {
return {
bsClass: 'alert',
bsStyle: 'info'
bsStyle: 'info',
closeLabel: 'Close Alert'
};
},

Expand All @@ -22,9 +24,9 @@ const Alert = React.createClass({
<button
type="button"
className="close"
onClick={this.props.onDismiss}
aria-hidden="true">
&times;
aria-label={this.props.closeLabel}
onClick={this.props.onDismiss}>
<span aria-hidden="true">&times;</span>
</button>
);
},
Expand All @@ -36,7 +38,7 @@ const Alert = React.createClass({
classes['alert-dismissable'] = isDismissable;

return (
<div {...this.props} className={classNames(this.props.className, classes)}>
<div {...this.props} role='alert' className={classNames(this.props.className, classes)}>
{isDismissable ? this.renderDismissButton() : null}
{this.props.children}
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/Nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const Nav = React.createClass({

return (
<nav {...this.props} className={classNames(this.props.className, classes)}>
{this.renderUl()}
{ this.renderUl() }
</nav>
);
},
Expand All @@ -67,7 +67,11 @@ const Nav = React.createClass({
classes['navbar-right'] = this.props.right;

return (
<ul {...this.props} className={classNames(this.props.className, classes)} ref="ul">
<ul {...this.props}
role={this.props.bsStyle === 'tabs' ? 'tablist' : null}
className={classNames(this.props.className, classes)}
ref="ul"
>
{ValidComponentChildren.map(this.props.children, this.renderNavItem)}
</ul>
);
Expand Down Expand Up @@ -95,6 +99,7 @@ const Nav = React.createClass({
return cloneElement(
child,
{
role: this.props.bsStyle === 'tabs' ? 'tab' : null,
active: this.getChildActiveProp(child),
activeKey: this.props.activeKey,
activeHref: this.props.activeHref,
Expand Down
16 changes: 12 additions & 4 deletions src/NavItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ const NavItem = React.createClass({
mixins: [BootstrapMixin],

propTypes: {
linkId: React.PropTypes.string,
onSelect: React.PropTypes.func,
active: React.PropTypes.bool,
disabled: React.PropTypes.bool,
href: React.PropTypes.string,
role: React.PropTypes.string,
title: React.PropTypes.node,
eventKey: React.PropTypes.any,
target: React.PropTypes.string
target: React.PropTypes.string,
'aria-controls': React.PropTypes.string
},

getDefaultProps() {
Expand All @@ -23,32 +26,37 @@ const NavItem = React.createClass({

render() {
let {
role,
linkId,
disabled,
active,
href,
title,
target,
children,
'aria-controls': ariaControls, // eslint-disable-line react/prop-types
...props } = this.props; // eslint-disable-line object-shorthand
let classes = {
active,
disabled
};
let linkProps = {
role,
href,
title,
target,
id: linkId,
onClick: this.handleClick,
ref: 'anchor'
};

if (href === '#') {
if (!role && href === '#') {
linkProps.role = 'button';
}

return (
<li {...props} className={classNames(props.className, classes)}>
<a {...linkProps}>
<li {...props} role='presentation' className={classNames(props.className, classes)}>
<a {...linkProps} aria-selected={active} aria-controls={ariaControls}>
{ children }
</a>
</li>
Expand Down
1 change: 1 addition & 0 deletions src/Panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const Panel = React.createClass({
return (
<a
href={'#' + (this.props.id || '')}
aria-controls={this.props.collapsible ? this.props.id : null}
className={this.isExpanded() ? null : 'collapsed'}
aria-expanded={this.isExpanded() ? 'true' : 'false'}
onClick={this.handleSelect}>
Expand Down
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
22 changes: 22 additions & 0 deletions test/AlertSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,26 @@ describe('Alert', function () {
);
assert.ok(React.findDOMNode(instance).className.match(/\balert-danger\b/));
});

describe('Web Accessibility', function(){
it('Should have alert role', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Alert>Message</Alert>
);

assert.equal(React.findDOMNode(instance).getAttribute('role'), 'alert');
});

it('Should have add ARIAs to button', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Alert onDismiss={()=>{}} closeLabel='close'>Message</Alert>
);

let button = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'button');

assert.equal(React.findDOMNode(button).getAttribute('aria-label'), 'close');
assert.equal(React.findDOMNode(button).children[0].getAttribute('aria-hidden'), 'true');
});

});
});
36 changes: 36 additions & 0 deletions test/NavItemSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,40 @@ describe('NavItem', function () {
let linkElement = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
assert.equal(linkElement.outerHTML.match('role="button"'), null);
});

describe('Web Accessibility', function(){

it('Should pass aria-controls to the link', function () {
let instance = ReactTestUtils.renderIntoDocument(
<NavItem href="/path/to/stuff" target="_blank" aria-controls='hi'>Item content</NavItem>
);

let linkElement = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));

assert.ok(linkElement.hasAttribute('aria-controls'));
});

it('Should add aria-selected to the link', function () {
let instance = ReactTestUtils.renderIntoDocument(
<NavItem active>Item content</NavItem>
);

let linkElement = React.findDOMNode(
ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));

assert.equal(linkElement.getAttribute('aria-selected'), 'true');
});

it('Should pass role down', function () {
let instance = ReactTestUtils.renderIntoDocument(
<NavItem role='tab'>Item content</NavItem>
);

let linkElement = React.findDOMNode(
ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));

assert.equal(linkElement.getAttribute('role'), 'tab');
});
});

});
19 changes: 19 additions & 0 deletions test/NavSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,23 @@ describe('Nav', function () {

assert.ok(items[0].props.navItem);
});


describe('Web Accessibility', function(){

it('Should have tablist and tab roles', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Nav bsStyle="tabs" activeKey={1}>
<NavItem key={1}>Tab 1 content</NavItem>
<NavItem key={2}>Tab 2 content</NavItem>
</Nav>
);

let ul = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'ul')[0];
let navItem = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'a')[0];

assert.equal(React.findDOMNode(ul).getAttribute('role'), 'tablist');
assert.equal(React.findDOMNode(navItem).getAttribute('role'), 'tab');
});
});
});
56 changes: 36 additions & 20 deletions test/PanelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,26 +123,6 @@ describe('Panel', function () {
assert.ok(anchor.className.match(/\bcollapsed\b/));
});

it('Should be aria-expanded=true', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Panel collapsible={true} expanded={true} header="Heading">Panel content</Panel>
);
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
assert.equal(collapse.getAttribute('aria-expanded'), 'true');
assert.equal(anchor.getAttribute('aria-expanded'), 'true');
});

it('Should be aria-expanded=false', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Panel collapsible={true} expanded={false} header="Heading">Panel content</Panel>
);
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
assert.equal(collapse.getAttribute('aria-expanded'), 'false');
assert.equal(anchor.getAttribute('aria-expanded'), 'false');
});

it('Should call onSelect handler', function (done) {
function handleSelect (e, key) {
assert.equal(key, '1');
Expand Down Expand Up @@ -204,4 +184,40 @@ describe('Panel', function () {
assert.equal(children[0].nodeName, 'TABLE');
assert.notOk(children[0].className.match(/\bpanel-body\b/));
});

describe('Web Accessibility', function(){

it('Should be aria-expanded=true', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Panel collapsible={true} expanded={true} header="Heading">Panel content</Panel>
);
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
assert.equal(collapse.getAttribute('aria-expanded'), 'true');
assert.equal(anchor.getAttribute('aria-expanded'), 'true');
});

it('Should be aria-expanded=false', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Panel collapsible={true} expanded={false} header="Heading">Panel content</Panel>
);
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
assert.equal(collapse.getAttribute('aria-expanded'), 'false');
assert.equal(anchor.getAttribute('aria-expanded'), 'false');
});

it('Should add aria-controls with id', function () {
let instance = ReactTestUtils.renderIntoDocument(
<Panel id='panel-1' collapsible expanded header="Heading">Panel content</Panel>
);

let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');

assert.equal(collapse.getAttribute('id'), 'panel-1');
assert.equal(anchor.getAttribute('aria-controls'), 'panel-1');
});

});
});
Loading

0 comments on commit 903b5d1

Please sign in to comment.