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:
@@ -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">
|
||||||
|
<button
|
||||||
|
aria-controls={examplePanelId}
|
||||||
|
aria-selected={this.state.activeTab === "example"}
|
||||||
|
className="tablinks"
|
||||||
|
data-name="example"
|
||||||
|
id={exampleTabId}
|
||||||
|
onClick={ this.activeTab }
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
{isExecute ? "Edit Value" : "Example Value"}
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{ schema ? <li className={ "tabitem" + ( this.state.activeTab === "model" ? " active" : "") }>
|
{ schema && (
|
||||||
<a className={ "tablinks" + ( isExecute ? " inactive" : "" )} data-name="model" onClick={ this.activeTab }>
|
<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" }
|
{isOAS3 ? "Schema" : "Model" }
|
||||||
</a>
|
</button>
|
||||||
</li> : null }
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<div>
|
{this.state.activeTab === "example" && (
|
||||||
{
|
<div
|
||||||
this.state.activeTab === "example" ? (
|
aria-hidden={this.state.activeTab !== "example"}
|
||||||
example ? 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 }
|
{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 }
|
getComponent={ getComponent }
|
||||||
getConfigs={ getConfigs }
|
getConfigs={ getConfigs }
|
||||||
specSelectors={ specSelectors }
|
specSelectors={ specSelectors }
|
||||||
expandDepth={ defaultModelExpandDepth }
|
expandDepth={ defaultModelExpandDepth }
|
||||||
specPath={specPath}
|
specPath={specPath}
|
||||||
includeReadOnly = {includeReadOnly}
|
includeReadOnly = {includeReadOnly}
|
||||||
includeWriteOnly = {includeWriteOnly}/>
|
includeWriteOnly = {includeWriteOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
test/e2e-cypress/tests/a11y/response-tabs.js
Normal file
35
test/e2e-cypress/tests/a11y/response-tabs.js
Normal 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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user