Skip to content

Commit 769781d

Browse files
d-reinholdKenny Wang
authored and
Kenny Wang
committedAug 20, 2015
[added] accessibility props for PanelGroup and Panels.
Signed-off-by: Kenny Wang <[email protected]>
1 parent 136d442 commit 769781d

File tree

3 files changed

+84
-18
lines changed

3 files changed

+84
-18
lines changed
 

‎src/Panel.js

+24-16
Original file line numberDiff line numberDiff line change
@@ -60,26 +60,32 @@ const Panel = React.createClass({
6060
},
6161

6262
render() {
63+
let {headerRole, panelRole, ...props} = this.props;
6364
return (
64-
<div {...this.props}
65+
<div {...props}
6566
className={classNames(this.props.className, this.getBsClassSet())}
6667
id={this.props.collapsible ? null : this.props.id} onSelect={null}>
67-
{this.renderHeading()}
68-
{this.props.collapsible ? this.renderCollapsibleBody() : this.renderBody()}
68+
{this.renderHeading(headerRole)}
69+
{this.props.collapsible ? this.renderCollapsibleBody(panelRole) : this.renderBody()}
6970
{this.renderFooter()}
7071
</div>
7172
);
7273
},
7374

74-
renderCollapsibleBody() {
75-
let collapseClass = this.prefixClass('collapse');
75+
renderCollapsibleBody(panelRole) {
76+
let props = {
77+
className: this.prefixClass('collapse'),
78+
id: this.props.id,
79+
ref: 'panel',
80+
'aria-hidden': !this.isExpanded()
81+
};
82+
if (panelRole) {
83+
props.role = panelRole;
84+
}
7685

7786
return (
7887
<Collapse in={this.isExpanded()}>
79-
<div
80-
className={collapseClass}
81-
id={this.props.id}
82-
ref='panel'>
88+
<div {...props}>
8389
{this.renderBody()}
8490

8591
</div>
@@ -148,7 +154,7 @@ const Panel = React.createClass({
148154
return React.isValidElement(child) && child.props.fill != null;
149155
},
150156

151-
renderHeading() {
157+
renderHeading(headerRole) {
152158
let header = this.props.header;
153159

154160
if (!header) {
@@ -157,7 +163,7 @@ const Panel = React.createClass({
157163

158164
if (!React.isValidElement(header) || Array.isArray(header)) {
159165
header = this.props.collapsible ?
160-
this.renderCollapsibleTitle(header) : header;
166+
this.renderCollapsibleTitle(header, headerRole) : header;
161167
} else {
162168
const className = classNames(
163169
this.prefixClass('title'), header.props.className
@@ -166,7 +172,7 @@ const Panel = React.createClass({
166172
if (this.props.collapsible) {
167173
header = cloneElement(header, {
168174
className,
169-
children: this.renderAnchor(header.props.children)
175+
children: this.renderAnchor(header.props.children, headerRole)
170176
});
171177
} else {
172178
header = cloneElement(header, {className});
@@ -180,23 +186,25 @@ const Panel = React.createClass({
180186
);
181187
},
182188

183-
renderAnchor(header) {
189+
renderAnchor(header, headerRole) {
184190
return (
185191
<a
186192
href={'#' + (this.props.id || '')}
187193
aria-controls={this.props.collapsible ? this.props.id : null}
188194
className={this.isExpanded() ? null : 'collapsed'}
189195
aria-expanded={this.isExpanded()}
190-
onClick={this.handleSelect}>
196+
aria-selected={this.isExpanded()}
197+
onClick={this.handleSelect}
198+
role={headerRole}>
191199
{header}
192200
</a>
193201
);
194202
},
195203

196-
renderCollapsibleTitle(header) {
204+
renderCollapsibleTitle(header, headerRole) {
197205
return (
198206
<h4 className={this.prefixClass('title')}>
199-
{this.renderAnchor(header)}
207+
{this.renderAnchor(header, headerRole)}
200208
</h4>
201209
);
202210
},

‎src/PanelGroup.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ const PanelGroup = React.createClass({
3535

3636
render() {
3737
let classes = this.getBsClassSet();
38+
let {className, ...props} = this.props;
39+
if (this.props.accordion) { props.role = 'tablist'; }
3840
return (
39-
<div {...this.props} className={classNames(this.props.className, classes)} onSelect={null}>
40-
{ValidComponentChildren.map(this.props.children, this.renderPanel)}
41+
<div {...props} className={classNames(className, classes)} onSelect={null}>
42+
{ValidComponentChildren.map(props.children, this.renderPanel)}
4143
</div>
4244
);
4345
},
@@ -53,6 +55,8 @@ const PanelGroup = React.createClass({
5355
};
5456

5557
if (this.props.accordion) {
58+
props.headerRole = 'tab';
59+
props.panelRole = 'tabpanel';
5660
props.collapsible = true;
5761
props.expanded = (child.props.eventKey === activeKey);
5862
props.onSelect = this.handleSelect;

‎test/PanelGroupSpec.js

+54
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,58 @@ describe('PanelGroup', function () {
4747

4848
assert.notOk(panel.state.collapsing);
4949
});
50+
51+
describe('Web Accessibility', function() {
52+
let instance, panelBodies, panelGroup, links;
53+
54+
beforeEach(function() {
55+
instance = ReactTestUtils.renderIntoDocument(
56+
<PanelGroup defaultActiveKey='1' accordion>
57+
<Panel header='Collapsible Group Item #1' eventKey='1' id='Panel1ID'>Panel 1</Panel>
58+
<Panel header='Collapsible Group Item #2' eventKey='2' id='Panel2ID'>Panel 2</Panel>
59+
</PanelGroup>
60+
);
61+
let accordion = ReactTestUtils.findRenderedComponentWithType(instance, PanelGroup);
62+
panelGroup = ReactTestUtils.findRenderedDOMComponentWithClass(accordion, 'panel-group');
63+
panelBodies = ReactTestUtils.scryRenderedDOMComponentsWithClass(panelGroup, 'panel-collapse');
64+
links = ReactTestUtils.scryRenderedDOMComponentsWithClass(panelGroup, 'panel-heading')
65+
.map(function(header) {
66+
return ReactTestUtils.findRenderedDOMComponentWithTag(header, 'a');
67+
});
68+
});
69+
70+
it('Should have a role of tablist', function() {
71+
assert.equal(panelGroup.props.role, 'tablist');
72+
});
73+
74+
it('Should provide each header tab with role of tab', function() {
75+
assert.equal(links[0].props.role, 'tab');
76+
assert.equal(links[1].props.role, 'tab');
77+
});
78+
79+
it('Should provide the panelBodies with role of tabpanel', function() {
80+
assert.equal(panelBodies[0].props.role, 'tabpanel');
81+
});
82+
83+
it('Should provide each panel with an aria-labelledby referencing the corresponding header', function() {
84+
assert.equal(panelBodies[0].props.id, links[0].props['aria-controls']);
85+
assert.equal(panelBodies[1].props.id, links[1].props['aria-controls']);
86+
});
87+
88+
it('Should maintain each tab aria-selected state', function() {
89+
assert.equal(links[0].props['aria-selected'], true);
90+
assert.equal(links[1].props['aria-selected'], false);
91+
});
92+
93+
it('Should maintain each tab aria-hidden state', function() {
94+
assert.equal(panelBodies[0].props['aria-hidden'], false);
95+
assert.equal(panelBodies[1].props['aria-hidden'], true);
96+
});
97+
98+
afterEach(function() {
99+
if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) {
100+
React.unmountComponentAtNode(React.findDOMNode(instance));
101+
}
102+
});
103+
});
50104
});

0 commit comments

Comments
 (0)
Please sign in to comment.