Skip to content

Commit 903b5d1

Browse files
committed
Merge pull request react-bootstrap#857 from react-bootstrap/accessibility
Accessibility
2 parents 3110e0b + ccc50e0 commit 903b5d1

File tree

12 files changed

+226
-37
lines changed

12 files changed

+226
-37
lines changed

src/Alert.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ const Alert = React.createClass({
77

88
propTypes: {
99
onDismiss: React.PropTypes.func,
10-
dismissAfter: React.PropTypes.number
10+
dismissAfter: React.PropTypes.number,
11+
closeLabel: React.PropTypes.string
1112
},
1213

1314
getDefaultProps() {
1415
return {
1516
bsClass: 'alert',
16-
bsStyle: 'info'
17+
bsStyle: 'info',
18+
closeLabel: 'Close Alert'
1719
};
1820
},
1921

@@ -22,9 +24,9 @@ const Alert = React.createClass({
2224
<button
2325
type="button"
2426
className="close"
25-
onClick={this.props.onDismiss}
26-
aria-hidden="true">
27-
&times;
27+
aria-label={this.props.closeLabel}
28+
onClick={this.props.onDismiss}>
29+
<span aria-hidden="true">&times;</span>
2830
</button>
2931
);
3032
},
@@ -36,7 +38,7 @@ const Alert = React.createClass({
3638
classes['alert-dismissable'] = isDismissable;
3739

3840
return (
39-
<div {...this.props} className={classNames(this.props.className, classes)}>
41+
<div {...this.props} role='alert' className={classNames(this.props.className, classes)}>
4042
{isDismissable ? this.renderDismissButton() : null}
4143
{this.props.children}
4244
</div>

src/Nav.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const Nav = React.createClass({
5252

5353
return (
5454
<nav {...this.props} className={classNames(this.props.className, classes)}>
55-
{this.renderUl()}
55+
{ this.renderUl() }
5656
</nav>
5757
);
5858
},
@@ -67,7 +67,11 @@ const Nav = React.createClass({
6767
classes['navbar-right'] = this.props.right;
6868

6969
return (
70-
<ul {...this.props} className={classNames(this.props.className, classes)} ref="ul">
70+
<ul {...this.props}
71+
role={this.props.bsStyle === 'tabs' ? 'tablist' : null}
72+
className={classNames(this.props.className, classes)}
73+
ref="ul"
74+
>
7175
{ValidComponentChildren.map(this.props.children, this.renderNavItem)}
7276
</ul>
7377
);
@@ -95,6 +99,7 @@ const Nav = React.createClass({
9599
return cloneElement(
96100
child,
97101
{
102+
role: this.props.bsStyle === 'tabs' ? 'tab' : null,
98103
active: this.getChildActiveProp(child),
99104
activeKey: this.props.activeKey,
100105
activeHref: this.props.activeHref,

src/NavItem.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ const NavItem = React.createClass({
66
mixins: [BootstrapMixin],
77

88
propTypes: {
9+
linkId: React.PropTypes.string,
910
onSelect: React.PropTypes.func,
1011
active: React.PropTypes.bool,
1112
disabled: React.PropTypes.bool,
1213
href: React.PropTypes.string,
14+
role: React.PropTypes.string,
1315
title: React.PropTypes.node,
1416
eventKey: React.PropTypes.any,
15-
target: React.PropTypes.string
17+
target: React.PropTypes.string,
18+
'aria-controls': React.PropTypes.string
1619
},
1720

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

2427
render() {
2528
let {
29+
role,
30+
linkId,
2631
disabled,
2732
active,
2833
href,
2934
title,
3035
target,
3136
children,
37+
'aria-controls': ariaControls, // eslint-disable-line react/prop-types
3238
...props } = this.props; // eslint-disable-line object-shorthand
3339
let classes = {
3440
active,
3541
disabled
3642
};
3743
let linkProps = {
44+
role,
3845
href,
3946
title,
4047
target,
48+
id: linkId,
4149
onClick: this.handleClick,
4250
ref: 'anchor'
4351
};
4452

45-
if (href === '#') {
53+
if (!role && href === '#') {
4654
linkProps.role = 'button';
4755
}
4856

4957
return (
50-
<li {...props} className={classNames(props.className, classes)}>
51-
<a {...linkProps}>
58+
<li {...props} role='presentation' className={classNames(props.className, classes)}>
59+
<a {...linkProps} aria-selected={active} aria-controls={ariaControls}>
5260
{ children }
5361
</a>
5462
</li>

src/Panel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ const Panel = React.createClass({
176176
return (
177177
<a
178178
href={'#' + (this.props.id || '')}
179+
aria-controls={this.props.collapsible ? this.props.id : null}
179180
className={this.isExpanded() ? null : 'collapsed'}
180181
aria-expanded={this.isExpanded() ? 'true' : 'false'}
181182
onClick={this.handleSelect}>

src/TabPane.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ const TabPane = React.createClass({
7878
};
7979

8080
return (
81-
<div {...this.props} className={classNames(this.props.className, classes)}>
81+
<div {...this.props}
82+
role='tabpanel'
83+
aria-hidden={!this.props.active}
84+
className={classNames(this.props.className, classes)}
85+
>
8286
{this.props.children}
8387
</div>
8488
);

src/TabbedArea.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import ValidComponentChildren from './utils/ValidComponentChildren';
55
import Nav from './Nav';
66
import NavItem from './NavItem';
77

8+
let panelId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___panel___' + child.props.eventKey);
9+
let tabId = (props, child) => child.props.id ? child.props.id + '___tab' : props.id && (props.id + '___tab___' + child.props.eventKey);
10+
811
function getDefaultActiveKeyFromChildren(children) {
912
let defaultActiveKey;
1013

@@ -61,6 +64,8 @@ const TabbedArea = React.createClass({
6164
},
6265

6366
render() {
67+
let { id, ...props } = this.props; // eslint-disable-line object-shorthand
68+
6469
let activeKey =
6570
this.props.activeKey != null ? this.props.activeKey : this.state.activeKey;
6671

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

7176
let nav = (
72-
<Nav {...this.props} activeKey={activeKey} onSelect={this.handleSelect} ref="tabs">
77+
<Nav {...props} activeKey={activeKey} onSelect={this.handleSelect} ref="tabs">
7378
{ValidComponentChildren.map(this.props.children, renderTabIfSet, this)}
7479
</Nav>
7580
);
7681

7782
return (
7883
<div>
7984
{nav}
80-
<div id={this.props.id} className="tab-content" ref="panes">
85+
<div id={id} className="tab-content" ref="panes">
8186
{ValidComponentChildren.map(this.props.children, this.renderPane)}
8287
</div>
8388
</div>
@@ -91,11 +96,15 @@ const TabbedArea = React.createClass({
9196
renderPane(child, index) {
9297
let activeKey = this.getActiveKey();
9398

99+
let active = (child.props.eventKey === activeKey &&
100+
(this.state.previousActiveKey == null || !this.props.animation));
101+
94102
return cloneElement(
95103
child,
96104
{
97-
active: (child.props.eventKey === activeKey &&
98-
(this.state.previousActiveKey == null || !this.props.animation)),
105+
active,
106+
id: panelId(this.props, child),
107+
'aria-labelledby': tabId(this.props, child),
99108
key: child.key ? child.key : index,
100109
animation: this.props.animation,
101110
onAnimateOutEnd: (this.state.previousActiveKey != null &&
@@ -106,9 +115,12 @@ const TabbedArea = React.createClass({
106115

107116
renderTab(child) {
108117
let {eventKey, className, tab, disabled } = child.props;
118+
109119
return (
110120
<NavItem
121+
linkId={tabId(this.props, child)}
111122
ref={'tab' + eventKey}
123+
aria-controls={panelId(this.props, child)}
112124
eventKey={eventKey}
113125
className={className}
114126
disabled={disabled}>

test/AlertSpec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,26 @@ describe('Alert', function () {
7171
);
7272
assert.ok(React.findDOMNode(instance).className.match(/\balert-danger\b/));
7373
});
74+
75+
describe('Web Accessibility', function(){
76+
it('Should have alert role', function () {
77+
let instance = ReactTestUtils.renderIntoDocument(
78+
<Alert>Message</Alert>
79+
);
80+
81+
assert.equal(React.findDOMNode(instance).getAttribute('role'), 'alert');
82+
});
83+
84+
it('Should have add ARIAs to button', function () {
85+
let instance = ReactTestUtils.renderIntoDocument(
86+
<Alert onDismiss={()=>{}} closeLabel='close'>Message</Alert>
87+
);
88+
89+
let button = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'button');
90+
91+
assert.equal(React.findDOMNode(button).getAttribute('aria-label'), 'close');
92+
assert.equal(React.findDOMNode(button).children[0].getAttribute('aria-hidden'), 'true');
93+
});
94+
95+
});
7496
});

test/NavItemSpec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,40 @@ describe('NavItem', function () {
106106
let linkElement = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
107107
assert.equal(linkElement.outerHTML.match('role="button"'), null);
108108
});
109+
110+
describe('Web Accessibility', function(){
111+
112+
it('Should pass aria-controls to the link', function () {
113+
let instance = ReactTestUtils.renderIntoDocument(
114+
<NavItem href="/path/to/stuff" target="_blank" aria-controls='hi'>Item content</NavItem>
115+
);
116+
117+
let linkElement = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
118+
119+
assert.ok(linkElement.hasAttribute('aria-controls'));
120+
});
121+
122+
it('Should add aria-selected to the link', function () {
123+
let instance = ReactTestUtils.renderIntoDocument(
124+
<NavItem active>Item content</NavItem>
125+
);
126+
127+
let linkElement = React.findDOMNode(
128+
ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
129+
130+
assert.equal(linkElement.getAttribute('aria-selected'), 'true');
131+
});
132+
133+
it('Should pass role down', function () {
134+
let instance = ReactTestUtils.renderIntoDocument(
135+
<NavItem role='tab'>Item content</NavItem>
136+
);
137+
138+
let linkElement = React.findDOMNode(
139+
ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
140+
141+
assert.equal(linkElement.getAttribute('role'), 'tab');
142+
});
143+
});
144+
109145
});

test/NavSpec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,23 @@ describe('Nav', function () {
114114

115115
assert.ok(items[0].props.navItem);
116116
});
117+
118+
119+
describe('Web Accessibility', function(){
120+
121+
it('Should have tablist and tab roles', function () {
122+
let instance = ReactTestUtils.renderIntoDocument(
123+
<Nav bsStyle="tabs" activeKey={1}>
124+
<NavItem key={1}>Tab 1 content</NavItem>
125+
<NavItem key={2}>Tab 2 content</NavItem>
126+
</Nav>
127+
);
128+
129+
let ul = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'ul')[0];
130+
let navItem = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'a')[0];
131+
132+
assert.equal(React.findDOMNode(ul).getAttribute('role'), 'tablist');
133+
assert.equal(React.findDOMNode(navItem).getAttribute('role'), 'tab');
134+
});
135+
});
117136
});

test/PanelSpec.js

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,6 @@ describe('Panel', function () {
123123
assert.ok(anchor.className.match(/\bcollapsed\b/));
124124
});
125125

126-
it('Should be aria-expanded=true', function () {
127-
let instance = ReactTestUtils.renderIntoDocument(
128-
<Panel collapsible={true} expanded={true} header="Heading">Panel content</Panel>
129-
);
130-
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
131-
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
132-
assert.equal(collapse.getAttribute('aria-expanded'), 'true');
133-
assert.equal(anchor.getAttribute('aria-expanded'), 'true');
134-
});
135-
136-
it('Should be aria-expanded=false', function () {
137-
let instance = ReactTestUtils.renderIntoDocument(
138-
<Panel collapsible={true} expanded={false} header="Heading">Panel content</Panel>
139-
);
140-
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
141-
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
142-
assert.equal(collapse.getAttribute('aria-expanded'), 'false');
143-
assert.equal(anchor.getAttribute('aria-expanded'), 'false');
144-
});
145-
146126
it('Should call onSelect handler', function (done) {
147127
function handleSelect (e, key) {
148128
assert.equal(key, '1');
@@ -204,4 +184,40 @@ describe('Panel', function () {
204184
assert.equal(children[0].nodeName, 'TABLE');
205185
assert.notOk(children[0].className.match(/\bpanel-body\b/));
206186
});
187+
188+
describe('Web Accessibility', function(){
189+
190+
it('Should be aria-expanded=true', function () {
191+
let instance = ReactTestUtils.renderIntoDocument(
192+
<Panel collapsible={true} expanded={true} header="Heading">Panel content</Panel>
193+
);
194+
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
195+
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
196+
assert.equal(collapse.getAttribute('aria-expanded'), 'true');
197+
assert.equal(anchor.getAttribute('aria-expanded'), 'true');
198+
});
199+
200+
it('Should be aria-expanded=false', function () {
201+
let instance = ReactTestUtils.renderIntoDocument(
202+
<Panel collapsible={true} expanded={false} header="Heading">Panel content</Panel>
203+
);
204+
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
205+
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
206+
assert.equal(collapse.getAttribute('aria-expanded'), 'false');
207+
assert.equal(anchor.getAttribute('aria-expanded'), 'false');
208+
});
209+
210+
it('Should add aria-controls with id', function () {
211+
let instance = ReactTestUtils.renderIntoDocument(
212+
<Panel id='panel-1' collapsible expanded header="Heading">Panel content</Panel>
213+
);
214+
215+
let collapse = React.findDOMNode(instance).querySelector('.panel-collapse');
216+
let anchor = React.findDOMNode(instance).querySelector('.panel-title a');
217+
218+
assert.equal(collapse.getAttribute('id'), 'panel-1');
219+
assert.equal(anchor.getAttribute('aria-controls'), 'panel-1');
220+
});
221+
222+
});
207223
});

0 commit comments

Comments
 (0)