Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/wcag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: WCAG checks

on:
push:
branches: [ master, next ]
pull_request:
branches: [ master, next ]

env:
CYPRESS_CACHE_FOLDER: cypress/cache

permissions:
contents: read

jobs:
copy-to-clipboard-buttons:
name: Copy-to-clipboard button names
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc

- name: Cache Node Modules and Cypress binary
uses: actions/cache@v5
id: cache-primes
with:
path: |
node_modules
${{ env.CYPRESS_CACHE_FOLDER }}
key: ${{ runner.os }}-node-and-cypress-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
if: steps.cache-primes.outputs.cache-hit != 'true'
run: npm ci

- name: Run WCAG copy-button checks
run: npm run cy:wcag
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"cy:open": "cross-env BROWSERSLIST_ENV=browser-production cypress open",
"cy:run": "cross-env BROWSERSLIST_ENV=browser-production cypress run",
"cy:ci": "start-server-and-test cy:start http://localhost:3204 cy:run",
"cy:wcag": "start-server-and-test cy:start http://localhost:3204 'npm run cy:run -- --spec test/e2e-cypress/e2e/a11y/button-accessible-names.cy.js'",
"cy:dev": "start-server-and-test cy:start http://localhost:3204 cy:open",
"open-static": "npx open-cli http://localhost:3002",
"security-audit": "run-s -sc security-audit:all security-audit:prod",
Expand Down Expand Up @@ -127,6 +128,7 @@
"@release-it/conventional-changelog": "=11.0.0",
"@svgr/webpack": "=8.1.0",
"autoprefixer": "^10.4.21",
"axe-core": "=4.11.4",
"babel-loader": "^9.2.1",
"babel-plugin-lodash": "=3.3.4",
"babel-plugin-module-resolver": "=5.0.2",
Expand All @@ -139,6 +141,7 @@
"css-loader": "=7.1.2",
"cssnano": "=7.0.7",
"cypress": "=14.2.0",
"cypress-axe": "=1.7.0",
"dedent": "^1.6.0",
"deepmerge": "^4.3.1",
"enzyme": "=3.11.0",
Expand Down
16 changes: 14 additions & 2 deletions src/core/components/copy-to-clipboard-btn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
import PropTypes from "prop-types"

const COPY_PATH_LABEL = "Copy path to clipboard"

/**
* @param {{ getComponent: func, textToCopy: string }} props
* @returns {JSX.Element}
Expand All @@ -14,9 +16,19 @@ export default class CopyToClipboardBtn extends React.Component {
const CopyIcon = getComponent("CopyIcon")

return (
<div className="view-line-link copy-to-clipboard" title="Copy to clipboard">
<div
className="view-line-link copy-to-clipboard"
title={COPY_PATH_LABEL}
aria-label={COPY_PATH_LABEL}
>
<CopyToClipboard text={this.props.textToCopy}>
<CopyIcon />
<button
aria-label={COPY_PATH_LABEL}
title={COPY_PATH_LABEL}
type="button"
>
<CopyIcon />
</button>
</CopyToClipboard>
</div>
)
Expand Down
16 changes: 14 additions & 2 deletions src/core/components/curl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import PropTypes from "prop-types"
import { CopyToClipboard } from "react-copy-to-clipboard"
import { requestSnippetGenerator_curl_bash } from "../plugins/request-snippets/fn"

const COPY_CURL_COMMAND_LABEL = "Copy cURL command to clipboard"

export default class Curl extends React.Component {
static propTypes = {
getComponent: PropTypes.func.isRequired,
Expand All @@ -17,8 +19,18 @@ export default class Curl extends React.Component {
return (
<div className="curl-command">
<h4>Curl</h4>
<div className="copy-to-clipboard">
<CopyToClipboard text={curl}><button/></CopyToClipboard>
<div
className="copy-to-clipboard"
title={COPY_CURL_COMMAND_LABEL}
aria-label={COPY_CURL_COMMAND_LABEL}
>
<CopyToClipboard text={curl}>
<button
aria-label={COPY_CURL_COMMAND_LABEL}
title={COPY_CURL_COMMAND_LABEL}
type="button"
/>
</CopyToClipboard>
</div>
<div>
<SyntaxHighlighter
Expand Down
14 changes: 12 additions & 2 deletions src/core/plugins/request-snippets/request-snippets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import classNames from "classnames"
import PropTypes from "prop-types"
import { CopyToClipboard } from "react-copy-to-clipboard"

const COPY_REQUEST_SNIPPET_LABEL = "Copy request snippet to clipboard"

const style = {
cursor: "pointer",
lineHeight: 1,
Expand Down Expand Up @@ -131,9 +133,17 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getComponent }) =>
})
}
</div>
<div className="copy-to-clipboard">
<div
className="copy-to-clipboard"
title={COPY_REQUEST_SNIPPET_LABEL}
aria-label={COPY_REQUEST_SNIPPET_LABEL}
>
<CopyToClipboard text={snippet}>
<button />
<button
aria-label={COPY_REQUEST_SNIPPET_LABEL}
title={COPY_REQUEST_SNIPPET_LABEL}
type="button"
/>
</CopyToClipboard>
</div>
<div>
Expand Down
14 changes: 12 additions & 2 deletions src/core/plugins/syntax-highlighting/components/HighlightCode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import classNames from "classnames"
import saveAs from "js-file-download"
import { CopyToClipboard } from "react-copy-to-clipboard"

const COPY_CODE_LABEL = "Copy code to clipboard"

const HighlightCode = ({
fileName = "response.txt",
className,
Expand Down Expand Up @@ -68,9 +70,17 @@ const HighlightCode = ({
return (
<div className="highlight-code" ref={rootRef}>
{canCopy && (
<div className="copy-to-clipboard">
<div
className="copy-to-clipboard"
title={COPY_CODE_LABEL}
aria-label={COPY_CODE_LABEL}
>
<CopyToClipboard text={children}>
<button />
<button
aria-label={COPY_CODE_LABEL}
title={COPY_CODE_LABEL}
type="button"
/>
</CopyToClipboard>
</div>
)}
Expand Down
8 changes: 7 additions & 1 deletion src/standalone/plugins/top-bar/components/DarkModeToggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ class DarkModeToggle extends Component {

render() {
const { isDarkMode } = this.state
const label = isDarkMode ? "Switch to light mode" : "Switch to dark mode"

return (
<div className="dark-mode-toggle">
<button onClick={this.toggleIsDarkMode}>
<button
aria-label={label}
title={label}
type="button"
onClick={this.toggleIsDarkMode}
>
{!isDarkMode ? (
<LightBulbOff height="24" />
) : (
Expand Down
70 changes: 70 additions & 0 deletions test/e2e-cypress/e2e/a11y/button-accessible-names.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @prettier
*/
const buttonNameRule = {
runOnly: {
type: "rule",
values: ["button-name"],
},
}

describe("Button accessible names", () => {
const checkAllButtonsHaveNames = () => {
cy.checkA11y("#swagger-ui", buttonNameRule)
}

const visitTestOperation = (query = "") => {
cy.visit(
`/?tryItOutEnabled=true${query}&url=/documents/features/try-it-out-enabled.yaml`
)
.injectAxe()
.get("#operations-default-get_")
.click()
}

it("gives the operation path copy button a programmatic name", () => {
visitTestOperation()

cy.get(".opblock-summary .copy-to-clipboard button")
.should("have.attr", "aria-label", "Copy path to clipboard")
.and("have.attr", "title", "Copy path to clipboard")

checkAllButtonsHaveNames()
})

it("gives response action buttons programmatic names", () => {
visitTestOperation()

cy.get(".btn.execute")
.click()
.get(".responses-wrapper .curl-command")
.should("be.visible")

cy.get(
'.responses-wrapper .copy-to-clipboard button[aria-label="Copy cURL command to clipboard"]'
)
.should("have.attr", "aria-label", "Copy cURL command to clipboard")
.and("have.attr", "title", "Copy cURL command to clipboard")

cy.get(".responses-wrapper .download-contents").should(
"have.text",
"Download"
)

checkAllButtonsHaveNames()
})

it("gives the request snippet copy button a programmatic name", () => {
visitTestOperation("&requestSnippetsEnabled=true")

cy.get(".btn.execute").click().get(".request-snippets").should("be.visible")

cy.get(
'.request-snippets .copy-to-clipboard button[aria-label="Copy request snippet to clipboard"]'
)
.should("have.attr", "aria-label", "Copy request snippet to clipboard")
.and("have.attr", "title", "Copy request snippet to clipboard")

checkAllButtonsHaveNames()
})
})
1 change: 1 addition & 0 deletions test/e2e-cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

// Import commands.js using ES2015 syntax:
import "./commands"
import "cypress-axe"

// Alternatively you can use CommonJS syntax:
// require('./commands')
Expand Down