diff --git a/src/Panel.js b/src/Panel.js index 2e597fc509..53c5f9e36a 100644 --- a/src/Panel.js +++ b/src/Panel.js @@ -60,26 +60,32 @@ const Panel = React.createClass({ }, render() { + let {headerRole, panelRole, ...props} = this.props; return ( -
- {this.renderHeading()} - {this.props.collapsible ? this.renderCollapsibleBody() : this.renderBody()} + {this.renderHeading(headerRole)} + {this.props.collapsible ? this.renderCollapsibleBody(panelRole) : this.renderBody()} {this.renderFooter()}
); }, - renderCollapsibleBody() { - let collapseClass = this.prefixClass('collapse'); + renderCollapsibleBody(panelRole) { + let props = { + className: this.prefixClass('collapse'), + id: this.props.id, + ref: 'panel', + 'aria-hidden': !this.isExpanded() + }; + if (panelRole) { + props.role = panelRole; + } return ( -
+
{this.renderBody()}
@@ -148,7 +154,7 @@ const Panel = React.createClass({ return React.isValidElement(child) && child.props.fill != null; }, - renderHeading() { + renderHeading(headerRole) { let header = this.props.header; if (!header) { @@ -157,7 +163,7 @@ const Panel = React.createClass({ if (!React.isValidElement(header) || Array.isArray(header)) { header = this.props.collapsible ? - this.renderCollapsibleTitle(header) : header; + this.renderCollapsibleTitle(header, headerRole) : header; } else { const className = classNames( this.prefixClass('title'), header.props.className @@ -166,7 +172,7 @@ const Panel = React.createClass({ if (this.props.collapsible) { header = cloneElement(header, { className, - children: this.renderAnchor(header.props.children) + children: this.renderAnchor(header.props.children, headerRole) }); } else { header = cloneElement(header, {className}); @@ -180,23 +186,25 @@ const Panel = React.createClass({ ); }, - renderAnchor(header) { + renderAnchor(header, headerRole) { return ( + aria-selected={this.isExpanded()} + onClick={this.handleSelect} + role={headerRole}> {header} ); }, - renderCollapsibleTitle(header) { + renderCollapsibleTitle(header, headerRole) { return (

- {this.renderAnchor(header)} + {this.renderAnchor(header, headerRole)}

); }, diff --git a/src/PanelGroup.js b/src/PanelGroup.js index 5ac0c55dab..16d5af2435 100644 --- a/src/PanelGroup.js +++ b/src/PanelGroup.js @@ -35,9 +35,11 @@ const PanelGroup = React.createClass({ render() { let classes = this.getBsClassSet(); + let {className, ...props} = this.props; + if (this.props.accordion) { props.role = 'tablist'; } return ( -
- {ValidComponentChildren.map(this.props.children, this.renderPanel)} +
+ {ValidComponentChildren.map(props.children, this.renderPanel)}
); }, @@ -53,6 +55,8 @@ const PanelGroup = React.createClass({ }; if (this.props.accordion) { + props.headerRole = 'tab'; + props.panelRole = 'tabpanel'; props.collapsible = true; props.expanded = (child.props.eventKey === activeKey); props.onSelect = this.handleSelect; diff --git a/test/PanelGroupSpec.js b/test/PanelGroupSpec.js index 43f8cbdae9..fc65496379 100644 --- a/test/PanelGroupSpec.js +++ b/test/PanelGroupSpec.js @@ -47,4 +47,58 @@ describe('PanelGroup', function () { assert.notOk(panel.state.collapsing); }); + + describe('Web Accessibility', function() { + let instance, panelBodies, panelGroup, links; + + beforeEach(function() { + instance = ReactTestUtils.renderIntoDocument( + + Panel 1 + Panel 2 + + ); + let accordion = ReactTestUtils.findRenderedComponentWithType(instance, PanelGroup); + panelGroup = ReactTestUtils.findRenderedDOMComponentWithClass(accordion, 'panel-group'); + panelBodies = ReactTestUtils.scryRenderedDOMComponentsWithClass(panelGroup, 'panel-collapse'); + links = ReactTestUtils.scryRenderedDOMComponentsWithClass(panelGroup, 'panel-heading') + .map(function(header) { + return ReactTestUtils.findRenderedDOMComponentWithTag(header, 'a'); + }); + }); + + it('Should have a role of tablist', function() { + assert.equal(panelGroup.props.role, 'tablist'); + }); + + it('Should provide each header tab with role of tab', function() { + assert.equal(links[0].props.role, 'tab'); + assert.equal(links[1].props.role, 'tab'); + }); + + it('Should provide the panelBodies with role of tabpanel', function() { + assert.equal(panelBodies[0].props.role, 'tabpanel'); + }); + + it('Should provide each panel with an aria-labelledby referencing the corresponding header', function() { + assert.equal(panelBodies[0].props.id, links[0].props['aria-controls']); + assert.equal(panelBodies[1].props.id, links[1].props['aria-controls']); + }); + + it('Should maintain each tab aria-selected state', function() { + assert.equal(links[0].props['aria-selected'], true); + assert.equal(links[1].props['aria-selected'], false); + }); + + it('Should maintain each tab aria-hidden state', function() { + assert.equal(panelBodies[0].props['aria-hidden'], false); + assert.equal(panelBodies[1].props['aria-hidden'], true); + }); + + afterEach(function() { + if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { + React.unmountComponentAtNode(React.findDOMNode(instance)); + } + }); + }); });