feat: Accessibility improvements (#7224)
* feat: adds a11y for ContentType & Responses region * feat: adds a11y to expandable/collapsible elements * fix: add aria label to select element for content types * fix: add aria label prop to contentType component * Change optag to h3 for better tag hierarchy Co-authored-by: ediiotero <eddie.otero@oddball.io> Co-authored-by: Mike Lumetta <mike.lumetta@adhocteam.us> Co-authored-by: Alexander Valencia <alex.valencia@adhocteam.us>
This commit is contained in:
@@ -8,7 +8,9 @@ const noop = ()=>{}
|
|||||||
export default class ContentType extends React.Component {
|
export default class ContentType extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
ariaControls: PropTypes.string,
|
||||||
contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set, ImPropTypes.seq]),
|
contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set, ImPropTypes.seq]),
|
||||||
|
controlId: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
@@ -41,14 +43,14 @@ export default class ContentType extends React.Component {
|
|||||||
onChangeWrapper = e => this.props.onChange(e.target.value)
|
onChangeWrapper = e => this.props.onChange(e.target.value)
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { contentTypes, className, value, ariaLabel } = this.props
|
let { ariaControls, ariaLabel, className, contentTypes, controlId, value } = this.props
|
||||||
|
|
||||||
if ( !contentTypes || !contentTypes.size )
|
if ( !contentTypes || !contentTypes.size )
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "content-type-wrapper " + ( className || "" ) }>
|
<div className={ "content-type-wrapper " + ( className || "" ) }>
|
||||||
<select className="content-type" aria-label={ariaLabel} value={value || ""} onChange={this.onChangeWrapper} >
|
<select aria-controls={ariaControls} aria-label={ariaLabel} className="content-type" id={controlId} onChange={this.onChangeWrapper} value={value || ""} >
|
||||||
{ contentTypes.map( (val) => {
|
{ contentTypes.map( (val) => {
|
||||||
return <option key={ val } value={ val }>{ val }</option>
|
return <option key={ val } value={ val }>{ val }</option>
|
||||||
}).toArray()}
|
}).toArray()}
|
||||||
|
|||||||
@@ -86,15 +86,13 @@ export default class ModelCollapse extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classes || ""} ref={this.onLoad}>
|
<span className={classes || ""} ref={this.onLoad}>
|
||||||
{ title && <span onClick={this.toggleCollapsed} className="pointer">{title}</span> }
|
<button aria-expanded={this.state.expanded} className="model-box-control" onClick={this.toggleCollapsed}>
|
||||||
<span onClick={ this.toggleCollapsed } className="pointer">
|
{ title && <span className="pointer">{title}</span> }
|
||||||
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span>
|
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span>
|
||||||
</span>
|
{ !this.state.expanded && <span>{this.state.collapsedContent}</span> }
|
||||||
{
|
</button>
|
||||||
this.state.expanded
|
|
||||||
? this.props.children
|
{ this.state.expanded && this.props.children }
|
||||||
: <span onClick={this.toggleCollapsed} className="pointer">{this.state.collapsedContent}</span>
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,17 @@ export default class Models extends Component {
|
|||||||
const JumpToPath = getComponent("JumpToPath")
|
const JumpToPath = getComponent("JumpToPath")
|
||||||
|
|
||||||
return <section className={ showModels ? "models is-open" : "models"} ref={this.onLoadModels}>
|
return <section className={ showModels ? "models is-open" : "models"} ref={this.onLoadModels}>
|
||||||
<h4 onClick={() => layoutActions.show(specPathBase, !showModels)}>
|
<h4>
|
||||||
<span>{isOAS3 ? "Schemas" : "Models" }</span>
|
<button
|
||||||
<svg width="20" height="20">
|
aria-expanded={showModels}
|
||||||
<use xlinkHref={showModels ? "#large-arrow-down" : "#large-arrow"} />
|
className="models-control"
|
||||||
|
onClick={() => layoutActions.show(specPathBase, !showModels)}
|
||||||
|
>
|
||||||
|
<span>{isOAS3 ? "Schemas" : "Models"}</span>
|
||||||
|
<svg width="20" height="20" aria-hidden="true" focusable="false">
|
||||||
|
<use xlinkHref={showModels ? "#large-arrow-up" : "#large-arrow-down"} />
|
||||||
</svg>
|
</svg>
|
||||||
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<Collapse isOpened={showModels}>
|
<Collapse isOpened={showModels}>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default class OperationSummary extends PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
specPath: ImPropTypes.list.isRequired,
|
specPath: ImPropTypes.list.isRequired,
|
||||||
operationProps: PropTypes.instanceOf(Iterable).isRequired,
|
operationProps: PropTypes.instanceOf(Iterable).isRequired,
|
||||||
|
isShown: PropTypes.bool.isRequired,
|
||||||
toggleShown: PropTypes.func.isRequired,
|
toggleShown: PropTypes.func.isRequired,
|
||||||
getComponent: PropTypes.func.isRequired,
|
getComponent: PropTypes.func.isRequired,
|
||||||
getConfigs: PropTypes.func.isRequired,
|
getConfigs: PropTypes.func.isRequired,
|
||||||
@@ -26,6 +27,7 @@ export default class OperationSummary extends PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
isShown,
|
||||||
toggleShown,
|
toggleShown,
|
||||||
getComponent,
|
getComponent,
|
||||||
authActions,
|
authActions,
|
||||||
@@ -40,6 +42,7 @@ export default class OperationSummary extends PureComponent {
|
|||||||
method,
|
method,
|
||||||
op,
|
op,
|
||||||
showSummary,
|
showSummary,
|
||||||
|
path,
|
||||||
operationId,
|
operationId,
|
||||||
originalOperationId,
|
originalOperationId,
|
||||||
displayOperationId,
|
displayOperationId,
|
||||||
@@ -60,7 +63,13 @@ export default class OperationSummary extends PureComponent {
|
|||||||
const securityIsOptional = hasSecurity && security.size === 1 && security.first().isEmpty()
|
const securityIsOptional = hasSecurity && security.size === 1 && security.first().isEmpty()
|
||||||
const allowAnonymous = !hasSecurity || securityIsOptional
|
const allowAnonymous = !hasSecurity || securityIsOptional
|
||||||
return (
|
return (
|
||||||
<div className={`opblock-summary opblock-summary-${method}`} onClick={toggleShown} >
|
<div className={`opblock-summary opblock-summary-${method}`} >
|
||||||
|
<button
|
||||||
|
aria-label={`${method} ${path.replace(/\//g, "\u200b/")}`}
|
||||||
|
aria-expanded={isShown}
|
||||||
|
className="opblock-summary-control"
|
||||||
|
onClick={toggleShown}
|
||||||
|
>
|
||||||
<OperationSummaryMethod method={method} />
|
<OperationSummaryMethod method={method} />
|
||||||
<OperationSummaryPath getComponent={getComponent} operationProps={operationProps} specPath={specPath} />
|
<OperationSummaryPath getComponent={getComponent} operationProps={operationProps} specPath={specPath} />
|
||||||
|
|
||||||
@@ -72,6 +81,11 @@ export default class OperationSummary extends PureComponent {
|
|||||||
|
|
||||||
{displayOperationId && (originalOperationId || operationId) ? <span className="opblock-summary-operation-id">{originalOperationId || operationId}</span> : null}
|
{displayOperationId && (originalOperationId || operationId) ? <span className="opblock-summary-operation-id">{originalOperationId || operationId}</span> : null}
|
||||||
|
|
||||||
|
<svg className="arrow" width="20" height="20" aria-hidden="true" focusable="false">
|
||||||
|
<use href={isShown ? "#large-arrow-up" : "#large-arrow-down"} xlinkHref={isShown ? "#large-arrow-up" : "#large-arrow-down"} />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
{
|
{
|
||||||
allowAnonymous ? null :
|
allowAnonymous ? null :
|
||||||
<AuthorizeOperationBtn
|
<AuthorizeOperationBtn
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default class OperationTag extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className={showTag ? "opblock-tag-section is-open" : "opblock-tag-section"} >
|
<div className={showTag ? "opblock-tag-section is-open" : "opblock-tag-section"} >
|
||||||
|
|
||||||
<h4
|
<h3
|
||||||
onClick={() => layoutActions.show(isShownKey, !showTag)}
|
onClick={() => layoutActions.show(isShownKey, !showTag)}
|
||||||
className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }
|
className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }
|
||||||
id={isShownKey.map(v => escapeDeepLinkPath(v)).join("-")}
|
id={isShownKey.map(v => escapeDeepLinkPath(v)).join("-")}
|
||||||
@@ -105,15 +105,16 @@ export default class OperationTag extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
aria-expanded={showTag}
|
||||||
className="expand-operation"
|
className="expand-operation"
|
||||||
title={showTag ? "Collapse operation": "Expand operation"}
|
title={showTag ? "Collapse operation": "Expand operation"}
|
||||||
onClick={() => layoutActions.show(isShownKey, !showTag)}>
|
onClick={() => layoutActions.show(isShownKey, !showTag)}>
|
||||||
|
|
||||||
<svg className="arrow" width="20" height="20">
|
<svg className="arrow" width="20" height="20" aria-hidden="true" focusable="false">
|
||||||
<use href={showTag ? "#large-arrow-down" : "#large-arrow"} xlinkHref={showTag ? "#large-arrow-down" : "#large-arrow"} />
|
<use href={showTag ? "#large-arrow-up" : "#large-arrow-down"} xlinkHref={showTag ? "#large-arrow-up" : "#large-arrow-down"} />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</h4>
|
</h3>
|
||||||
|
|
||||||
<Collapse isOpened={showTag}>
|
<Collapse isOpened={showTag}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default class Operation extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={deprecated ? "opblock opblock-deprecated" : isShown ? `opblock opblock-${method} is-open` : `opblock opblock-${method}`} id={escapeDeepLinkPath(isShownKey.join("-"))} >
|
<div className={deprecated ? "opblock opblock-deprecated" : isShown ? `opblock opblock-${method} is-open` : `opblock opblock-${method}`} id={escapeDeepLinkPath(isShownKey.join("-"))} >
|
||||||
<OperationSummary operationProps={operationProps} toggleShown={toggleShown} getComponent={getComponent} authActions={authActions} authSelectors={authSelectors} specPath={specPath} />
|
<OperationSummary operationProps={operationProps} isShown={isShown} toggleShown={toggleShown} getComponent={getComponent} authActions={authActions} authSelectors={authSelectors} specPath={specPath} />
|
||||||
<Collapse isOpened={isShown}>
|
<Collapse isOpened={isShown}>
|
||||||
<div className="opblock-body">
|
<div className="opblock-body">
|
||||||
{ (operation && operation.size) || operation === null ? null :
|
{ (operation && operation.size) || operation === null ? null :
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { fromJS, Iterable } from "immutable"
|
|||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
import ImPropTypes from "react-immutable-proptypes"
|
import ImPropTypes from "react-immutable-proptypes"
|
||||||
import { defaultStatusCode, getAcceptControllingResponse } from "core/utils"
|
import { defaultStatusCode, getAcceptControllingResponse } from "core/utils"
|
||||||
|
import createHtmlReadyId from "../../helpers/create-html-ready-id"
|
||||||
|
|
||||||
export default class Responses extends React.Component {
|
export default class Responses extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -86,17 +87,22 @@ export default class Responses extends React.Component {
|
|||||||
const acceptControllingResponse = isSpecOAS3 ?
|
const acceptControllingResponse = isSpecOAS3 ?
|
||||||
getAcceptControllingResponse(responses) : null
|
getAcceptControllingResponse(responses) : null
|
||||||
|
|
||||||
|
const regionId = createHtmlReadyId(`${method}${path}_responses`)
|
||||||
|
const controlId = `${regionId}_select`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="responses-wrapper">
|
<div className="responses-wrapper">
|
||||||
<div className="opblock-section-header">
|
<div className="opblock-section-header">
|
||||||
<h4>Responses</h4>
|
<h4>Responses</h4>
|
||||||
{ specSelectors.isOAS3() ? null : <label>
|
{ specSelectors.isOAS3() ? null : <label htmlFor={controlId}>
|
||||||
<span>Response content type</span>
|
<span>Response content type</span>
|
||||||
<ContentType value={producesValue}
|
<ContentType value={producesValue}
|
||||||
onChange={this.onChangeProducesWrapper}
|
ariaControls={regionId}
|
||||||
contentTypes={produces}
|
ariaLabel="Response content type"
|
||||||
className="execute-content-type"
|
className="execute-content-type"
|
||||||
ariaLabel="Response content type" />
|
contentTypes={produces}
|
||||||
|
controlId={controlId}
|
||||||
|
onChange={this.onChangeProducesWrapper} />
|
||||||
</label> }
|
</label> }
|
||||||
</div>
|
</div>
|
||||||
<div className="responses-inner">
|
<div className="responses-inner">
|
||||||
@@ -115,7 +121,7 @@ export default class Responses extends React.Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<table className="responses-table">
|
<table aria-live="polite" className="responses-table" id={regionId} role="region">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="responses-header">
|
<tr className="responses-header">
|
||||||
<td className="col_header response-col_status">Code</td>
|
<td className="col_header response-col_status">Code</td>
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ const SvgAssets = () =>
|
|||||||
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
|
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 20 20" id="large-arrow-up">
|
||||||
|
<path d="M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
<symbol viewBox="0 0 24 24" id="jump-to">
|
<symbol viewBox="0 0 24 24" id="jump-to">
|
||||||
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
||||||
|
|||||||
10
src/helpers/create-html-ready-id.js
Normal file
10
src/helpers/create-html-ready-id.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Replace invalid characters from a string to create an html-ready ID
|
||||||
|
*
|
||||||
|
* @param {string} id A string that may contain invalid characters for the HTML ID attribute
|
||||||
|
* @param {string} [replacement=_] The string to replace invalid characters with; "_" by default
|
||||||
|
* @return {string} Information about the parameter schema
|
||||||
|
*/
|
||||||
|
export default function createHtmlReadyId(id, replacement = "_") {
|
||||||
|
return id.replace(/[^\w-]/g, replacement)
|
||||||
|
}
|
||||||
@@ -110,6 +110,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opblock-summary-control,
|
||||||
|
.models-control,
|
||||||
|
.model-box-control
|
||||||
|
{
|
||||||
|
all: inherit;
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.expand-methods,
|
.expand-methods,
|
||||||
.expand-operation
|
.expand-operation
|
||||||
{
|
{
|
||||||
@@ -143,11 +158,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
button
|
button
|
||||||
{
|
{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&.invalid
|
&.invalid
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ function ModelCollapseTest(baseUrl, urlFragment) {
|
|||||||
|
|
||||||
it("Models section should collapse and expand when toggled", () => {
|
it("Models section should collapse and expand when toggled", () => {
|
||||||
cy.visit(baseUrl)
|
cy.visit(baseUrl)
|
||||||
.get(".models h4")
|
.get(".models h4 .models-control")
|
||||||
.click()
|
.click()
|
||||||
.get(".models")
|
.get(".models")
|
||||||
.should("not.have.class", "is-open")
|
.should("not.have.class", "is-open")
|
||||||
.get("#model-Order")
|
.get("#model-Order")
|
||||||
.should("not.exist")
|
.should("not.exist")
|
||||||
.get(".models h4")
|
.get(".models h4 .models-control")
|
||||||
.click()
|
.click()
|
||||||
.get(".models")
|
.get(".models")
|
||||||
.should("have.class", "is-open")
|
.should("have.class", "is-open")
|
||||||
@@ -35,28 +35,15 @@ function ModelCollapseTest(baseUrl, urlFragment) {
|
|||||||
.should("exist")
|
.should("exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Model should collapse and expand when toggled clicking title", () => {
|
it("Model should collapse and expand when toggled clicking button", () => {
|
||||||
cy.visit(baseUrl)
|
cy.visit(baseUrl)
|
||||||
.get("#model-User .model-box .pointer:nth-child(1)")
|
.get("#model-User .model-box .model-box-control")
|
||||||
.click()
|
.click()
|
||||||
.get("#model-User .model-box .model .inner-object")
|
.get("#model-User .model-box .model .inner-object")
|
||||||
.should("exist")
|
.should("exist")
|
||||||
.get("#model-User .model-box .pointer:nth-child(1)")
|
.get("#model-User .model-box .model-box-control")
|
||||||
.click()
|
|
||||||
.get("#model-User .model-box .model .inner-object")
|
|
||||||
.should("not.exist")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Model should collapse and expand when toggled clicking arrow", () => {
|
|
||||||
cy.visit(baseUrl)
|
|
||||||
.get("#model-User .model-box .pointer:nth-child(2)")
|
|
||||||
.click()
|
|
||||||
.get("#model-User .model-box .model .inner-object")
|
|
||||||
.should("exist")
|
|
||||||
.get("#model-User .model-box .pointer:nth-child(2)")
|
|
||||||
.click()
|
.click()
|
||||||
.get("#model-User .model-box .model .inner-object")
|
.get("#model-User .model-box .model .inner-object")
|
||||||
.should("not.exist")
|
.should("not.exist")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe("<OperationTag/>", function(){
|
|||||||
|
|
||||||
const opblockTag = wrapper.find(".opblock-tag")
|
const opblockTag = wrapper.find(".opblock-tag")
|
||||||
expect(opblockTag.length).toEqual(1)
|
expect(opblockTag.length).toEqual(1)
|
||||||
|
expect(opblockTag.getNode().type).toEqual("h3")
|
||||||
|
|
||||||
const renderedLink = wrapper.find("Link")
|
const renderedLink = wrapper.find("Link")
|
||||||
expect(renderedLink.length).toEqual(1)
|
expect(renderedLink.length).toEqual(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user