From 72811bd827c829f52705f3f2814c4197f86cc1e1 Mon Sep 17 00:00:00 2001 From: Giles Wells Date: Wed, 12 May 2021 12:40:31 -0400 Subject: [PATCH] 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 Co-authored-by: Mike Lumetta Co-authored-by: Alexander Valencia --- src/core/components/content-type.jsx | 6 ++-- src/core/components/model-collapse.jsx | 14 ++++---- src/core/components/models.jsx | 16 +++++++--- src/core/components/operation-summary.jsx | 32 +++++++++++++------ src/core/components/operation-tag.jsx | 9 +++--- src/core/components/operation.jsx | 2 +- src/core/components/responses.jsx | 16 +++++++--- src/core/components/svg-assets.jsx | 3 ++ src/helpers/create-html-ready-id.js | 10 ++++++ src/style/_buttons.scss | 17 ++++++++-- .../tests/features/model-collapse.js | 25 ++++----------- test/unit/components/operation-tag.jsx | 1 + 12 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 src/helpers/create-html-ready-id.js diff --git a/src/core/components/content-type.jsx b/src/core/components/content-type.jsx index 6129d46d..22531c75 100644 --- a/src/core/components/content-type.jsx +++ b/src/core/components/content-type.jsx @@ -8,7 +8,9 @@ const noop = ()=>{} export default class ContentType extends React.Component { static propTypes = { + ariaControls: PropTypes.string, contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set, ImPropTypes.seq]), + controlId: PropTypes.string, value: PropTypes.string, onChange: PropTypes.func, className: PropTypes.string, @@ -41,14 +43,14 @@ export default class ContentType extends React.Component { onChangeWrapper = e => this.props.onChange(e.target.value) render() { - let { contentTypes, className, value, ariaLabel } = this.props + let { ariaControls, ariaLabel, className, contentTypes, controlId, value } = this.props if ( !contentTypes || !contentTypes.size ) return null return (
- { contentTypes.map( (val) => { return }).toArray()} diff --git a/src/core/components/model-collapse.jsx b/src/core/components/model-collapse.jsx index ef533bd4..97858084 100644 --- a/src/core/components/model-collapse.jsx +++ b/src/core/components/model-collapse.jsx @@ -86,15 +86,13 @@ export default class ModelCollapse extends Component { return ( - { title && {title} } - + + + { this.state.expanded && this.props.children } ) } diff --git a/src/core/components/models.jsx b/src/core/components/models.jsx index 2058b05b..e7a93f8b 100644 --- a/src/core/components/models.jsx +++ b/src/core/components/models.jsx @@ -58,11 +58,17 @@ export default class Models extends Component { const JumpToPath = getComponent("JumpToPath") return
-

layoutActions.show(specPathBase, !showModels)}> - {isOAS3 ? "Schemas" : "Models" } - - - +

+

{ diff --git a/src/core/components/operation-summary.jsx b/src/core/components/operation-summary.jsx index 8896f0d7..bdfafe7d 100644 --- a/src/core/components/operation-summary.jsx +++ b/src/core/components/operation-summary.jsx @@ -10,6 +10,7 @@ export default class OperationSummary extends PureComponent { static propTypes = { specPath: ImPropTypes.list.isRequired, operationProps: PropTypes.instanceOf(Iterable).isRequired, + isShown: PropTypes.bool.isRequired, toggleShown: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired, getConfigs: PropTypes.func.isRequired, @@ -26,6 +27,7 @@ export default class OperationSummary extends PureComponent { render() { let { + isShown, toggleShown, getComponent, authActions, @@ -40,6 +42,7 @@ export default class OperationSummary extends PureComponent { method, op, showSummary, + path, operationId, originalOperationId, displayOperationId, @@ -60,17 +63,28 @@ export default class OperationSummary extends PureComponent { const securityIsOptional = hasSecurity && security.size === 1 && security.first().isEmpty() const allowAnonymous = !hasSecurity || securityIsOptional return ( -
- - +
+ { allowAnonymous ? null : diff --git a/src/core/components/operation-tag.jsx b/src/core/components/operation-tag.jsx index 7eb89de7..da2edb12 100644 --- a/src/core/components/operation-tag.jsx +++ b/src/core/components/operation-tag.jsx @@ -70,7 +70,7 @@ export default class OperationTag extends React.Component { return (
-

layoutActions.show(isShownKey, !showTag)} className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" } id={isShownKey.map(v => escapeDeepLinkPath(v)).join("-")} @@ -105,15 +105,16 @@ export default class OperationTag extends React.Component {

- + {children} diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index 5fd8dc16..f5a799d9 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -114,7 +114,7 @@ export default class Operation extends PureComponent { return (
- +
{ (operation && operation.size) || operation === null ? null : diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index df479da0..8b73582c 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -3,6 +3,7 @@ import { fromJS, Iterable } from "immutable" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import { defaultStatusCode, getAcceptControllingResponse } from "core/utils" +import createHtmlReadyId from "../../helpers/create-html-ready-id" export default class Responses extends React.Component { static propTypes = { @@ -86,17 +87,22 @@ export default class Responses extends React.Component { const acceptControllingResponse = isSpecOAS3 ? getAcceptControllingResponse(responses) : null + const regionId = createHtmlReadyId(`${method}${path}_responses`) + const controlId = `${regionId}_select` + return (

Responses

- { specSelectors.isOAS3() ? null :
@@ -115,7 +121,7 @@ export default class Responses extends React.Component { } - +
diff --git a/src/core/components/svg-assets.jsx b/src/core/components/svg-assets.jsx index ca1c13ba..9db9517d 100644 --- a/src/core/components/svg-assets.jsx +++ b/src/core/components/svg-assets.jsx @@ -23,6 +23,9 @@ const SvgAssets = () => + + + diff --git a/src/helpers/create-html-ready-id.js b/src/helpers/create-html-ready-id.js new file mode 100644 index 00000000..0aa7c9ee --- /dev/null +++ b/src/helpers/create-html-ready-id.js @@ -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) +} diff --git a/src/style/_buttons.scss b/src/style/_buttons.scss index fd14989e..989ba22d 100644 --- a/src/style/_buttons.scss +++ b/src/style/_buttons.scss @@ -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-operation { @@ -143,11 +158,9 @@ } } - button { cursor: pointer; - outline: none; &.invalid { diff --git a/test/e2e-cypress/tests/features/model-collapse.js b/test/e2e-cypress/tests/features/model-collapse.js index c0b0d4f0..bc027257 100644 --- a/test/e2e-cypress/tests/features/model-collapse.js +++ b/test/e2e-cypress/tests/features/model-collapse.js @@ -18,16 +18,16 @@ function ModelCollapseTest(baseUrl, urlFragment) { .get("#model-Pet") .should("exist") }) - + it("Models section should collapse and expand when toggled", () => { cy.visit(baseUrl) - .get(".models h4") + .get(".models h4 .models-control") .click() .get(".models") .should("not.have.class", "is-open") .get("#model-Order") .should("not.exist") - .get(".models h4") + .get(".models h4 .models-control") .click() .get(".models") .should("have.class", "is-open") @@ -35,28 +35,15 @@ function ModelCollapseTest(baseUrl, urlFragment) { .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) - .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("exist") - .get("#model-User .model-box .pointer:nth-child(1)") - .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)") + .get("#model-User .model-box .model-box-control") .click() .get("#model-User .model-box .model .inner-object") .should("not.exist") }) } - diff --git a/test/unit/components/operation-tag.jsx b/test/unit/components/operation-tag.jsx index 773549e6..abe86368 100644 --- a/test/unit/components/operation-tag.jsx +++ b/test/unit/components/operation-tag.jsx @@ -44,6 +44,7 @@ describe("", function(){ const opblockTag = wrapper.find(".opblock-tag") expect(opblockTag.length).toEqual(1) + expect(opblockTag.getNode().type).toEqual("h3") const renderedLink = wrapper.find("Link") expect(renderedLink.length).toEqual(1)
Code