fix(a11n): provide proper a11n for response example tabs (#7464)

- Update tabs to use <button> elements instead of <a>
- Add aria roles for tablist, tabs, and tabpanel
- Add aria attributes for additional a11y compliance and screen reader accessibility
- Replace ids with data-name attribute for tabpanels
- Add cypress test 7463 and update swos-63
- Move tabs test file to tests/a11y directory
- Rename test file to be more descriptive of what is being tested.
- Add id attributes to both tabs and tabpanels to leverage aria-controls and aria-labelledby attributes

Co-authored-by: Calvin Gonzalez <calvin.gonzalez@oddball.io>
Co-authored-by: Vladimir Gorej <vladimir.gorej@gmail.com>

Closes #7463
Refs #7350
This commit is contained in:
Calvin Gonzalez
2021-09-17 02:19:55 -04:00
committed by GitHub
parent 00d5b30aa7
commit 8ffb1aef97
3 changed files with 112 additions and 34 deletions

View File

@@ -1,6 +1,8 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes" import ImPropTypes from "react-immutable-proptypes"
import cx from "classnames"
import randomBytes from "randombytes"
export default class ModelExample extends React.Component { export default class ModelExample extends React.Component {
static propTypes = { static propTypes = {
@@ -31,11 +33,11 @@ export default class ModelExample extends React.Component {
} }
this.state = { this.state = {
activeTab: activeTab activeTab,
} }
} }
activeTab =( e ) => { activeTab = ( e ) => {
let { target : { dataset : { name } } } = e let { target : { dataset : { name } } } = e
this.setState({ this.setState({
@@ -58,42 +60,83 @@ export default class ModelExample extends React.Component {
let { defaultModelExpandDepth } = getConfigs() let { defaultModelExpandDepth } = getConfigs()
const ModelWrapper = getComponent("ModelWrapper") const ModelWrapper = getComponent("ModelWrapper")
const HighlightCode = getComponent("highlightCode") const HighlightCode = getComponent("highlightCode")
const exampleTabId = randomBytes(5).toString("base64")
const examplePanelId = randomBytes(5).toString("base64")
const modelTabId = randomBytes(5).toString("base64")
const modelPanelId = randomBytes(5).toString("base64")
let isOAS3 = specSelectors.isOAS3() let isOAS3 = specSelectors.isOAS3()
return <div className="model-example"> return (
<ul className="tab"> <div className="model-example">
<li className={ "tabitem" + ( this.state.activeTab === "example" ? " active" : "") }> <ul className="tab" role="tablist">
<a className="tablinks" data-name="example" onClick={ this.activeTab }>{isExecute ? "Edit Value" : "Example Value"}</a> <li className={cx("tabitem", { active: this.state.activeTab === "example" })} role="presentation">
</li> <button
{ schema ? <li className={ "tabitem" + ( this.state.activeTab === "model" ? " active" : "") }> aria-controls={examplePanelId}
<a className={ "tablinks" + ( isExecute ? " inactive" : "" )} data-name="model" onClick={ this.activeTab }> aria-selected={this.state.activeTab === "example"}
{isOAS3 ? "Schema" : "Model" } className="tablinks"
</a> data-name="example"
</li> : null } id={exampleTabId}
</ul> onClick={ this.activeTab }
<div> role="tab"
{ >
this.state.activeTab === "example" ? ( {isExecute ? "Edit Value" : "Example Value"}
example ? example : ( </button>
</li>
{ schema && (
<li className={cx("tabitem", { active: this.state.activeTab === "model" })} role="presentation">
<button
aria-controls={modelPanelId}
aria-selected={this.state.activeTab === "model"}
className={cx("tablinks", { inactive: isExecute })}
data-name="model"
id={modelTabId}
onClick={ this.activeTab }
role="tab"
>
{isOAS3 ? "Schema" : "Model" }
</button>
</li>
)}
</ul>
{this.state.activeTab === "example" && (
<div
aria-hidden={this.state.activeTab !== "example"}
aria-labelledby={exampleTabId}
data-name="examplePanel"
id={examplePanelId}
role="tabpanel"
tabIndex="0"
>
{example ? example : (
<HighlightCode value="(no example available)" getConfigs={ getConfigs } /> <HighlightCode value="(no example available)" getConfigs={ getConfigs } />
) )}
) : null </div>
} )}
{
this.state.activeTab === "model" && <ModelWrapper schema={ schema }
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
expandDepth={ defaultModelExpandDepth }
specPath={specPath}
includeReadOnly = {includeReadOnly}
includeWriteOnly = {includeWriteOnly}/>
{this.state.activeTab === "model" && (
} <div
aria-hidden={this.state.activeTab === "example"}
aria-labelledby={modelTabId}
data-name="modelPanel"
id={modelPanelId}
role="tabpanel"
tabIndex="0"
>
<ModelWrapper
schema={ schema }
getComponent={ getComponent }
getConfigs={ getConfigs }
specSelectors={ specSelectors }
expandDepth={ defaultModelExpandDepth }
specPath={specPath}
includeReadOnly = {includeReadOnly}
includeWriteOnly = {includeWriteOnly}
/>
</div>
)}
</div> </div>
</div> )
} }
} }

View File

@@ -0,0 +1,35 @@
describe("Response tab elements", () => {
describe("ModelExample within Operation", () => {
it("should render Example tabpanel by default", () => {
cy
.visit("/?url=/documents/petstore-expanded.openapi.yaml")
.get("#operations-default-addPet")
.click()
.get("div[data-name=examplePanel]")
.first()
.should("have.attr", "aria-hidden", "false")
})
it("should click Schema tab button and render Schema tabpanel for OpenAPI 3", () => {
cy
.visit("/?url=/documents/petstore-expanded.openapi.yaml")
.get("#operations-default-addPet")
.click()
.get("button.tablinks[data-name=model]")
.first()
.click()
.get("div[data-name=modelPanel]")
.first()
.should("have.attr", "aria-hidden", "false")
})
it("should click Model tab button and render Model tabpanel for OpenAPI 2", () => {
cy
.visit("/?url=/documents/petstore.swagger.yaml")
.get("#operations-pet-addPet")
.click()
.get("button.tablinks[data-name=model]")
.click()
.get("div[data-name=modelPanel]")
.should("have.attr", "aria-hidden", "false")
})
})
})

View File

@@ -19,7 +19,7 @@ describe("SWOS-63: Schema/Model labeling", () => {
.visit("/?url=/documents/petstore-expanded.openapi.yaml") .visit("/?url=/documents/petstore-expanded.openapi.yaml")
.get("#operations-default-findPets") .get("#operations-default-findPets")
.click() .click()
.get("a.tablinks[data-name=model]") .get("button.tablinks[data-name=model]")
.contains("Schema") .contains("Schema")
}) })
it("should render `Models` for OpenAPI 2", () => { it("should render `Models` for OpenAPI 2", () => {
@@ -28,7 +28,7 @@ describe("SWOS-63: Schema/Model labeling", () => {
.get("section.models > h4") .get("section.models > h4")
.get("#operations-pet-addPet") .get("#operations-pet-addPet")
.click() .click()
.get("a.tablinks[data-name=model]") .get("button.tablinks[data-name=model]")
.contains("Model") .contains("Model")
}) })
}) })