Skip to content

Commit 42cbd70

Browse files
authored
feat: accessibility testing and improvements (#14454)
This PR adds automated accessibility testing into `test/a11y` and across a few test suites. To do list: - [x] Accessibility documentation - [x] Add coverage for all fields - [x] Add coverage for all major views and components - [x] Add coverage for plugins - [x] Lexical This PR adds: - new utility to run axe a11y scans on full pages in playwright - new utility to run checks for focus indicators - test to run checks for horizontal scroll overflow - a11y test suite for generic components testing - a11y tests across fields and various test suites - new test suite for textarea field in fields ### A11y report I've also created an a11y report with checklists for issues to tackle in this [discussion](#14489). If you're running test suites here make sure you turn on trace viewer to help debug any tests as we take screenshots of problematic elements and export a report of WCAG violations <img width="267" height="94" alt="image" src="https://github.com/user-attachments/assets/261aa167-bb41-4e26-8d5d-baee7ffa5de5" />
1 parent abe8563 commit 42cbd70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+8593
-1
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ jobs:
262262
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
263263
suite:
264264
- _community
265+
- a11y
265266
- access-control
266267
- admin__e2e__general
267268
- admin__e2e__list-view

docs/admin/accessibility.mdx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: Accessibility
3+
label: Accessibility
4+
order: 50
5+
desc: Our commitment and approach to accessibility within the admin panel.
6+
keywords: admin, accessibility, a11y, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
7+
---
8+
9+
Payload is committed to ensuring that our admin panel is accessible to all users, including those with disabilities. We follow best practices and guidelines to create an inclusive experience.
10+
11+
<Banner type="info">
12+
<p>
13+
We are actively working towards full compliance with WCAG 2.2 AA standards.
14+
If you encounter any accessibility issues, please report them in our{' '}
15+
<a
16+
href="https://github.com/payloadcms/payload/discussions/14489"
17+
target="_blank"
18+
rel="noopener noreferrer"
19+
>
20+
GitHub Discussion
21+
</a>{' '}
22+
page.
23+
</p>
24+
</Banner>
25+
26+
## Compliance standards
27+
28+
| Standard | Status | Description |
29+
| -------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------- |
30+
| [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) | In Progress | Web Content Accessibility Guidelines (WCAG) 2.2 AA is a widely recognized standard for web accessibility. |
31+
32+
You can view our [report](https://github.com/payloadcms/payload/discussions/14489) on the current state of the admin panel's accessibility compliance.
33+
34+
## Our approach
35+
36+
- Integrated Axe within our e2e test suites to ensure long term compliance.
37+
- Custom utilities to test keyboard navigation, window overflow and focus indicators across our components.
38+
- Manual testing with screen readers and other assistive technologies.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
"README.md": "sh -c 'cp ./README.md ./packages/payload/README.md'"
137137
},
138138
"devDependencies": {
139+
"@axe-core/playwright": "4.11.0",
139140
"@jest/globals": "29.7.0",
140141
"@libsql/client": "0.14.0",
141142
"@next/bundle-analyzer": "15.4.7",
@@ -156,6 +157,7 @@
156157
"@types/react": "19.1.12",
157158
"@types/react-dom": "19.1.9",
158159
"@types/shelljs": "0.8.15",
160+
"axe-core": "4.11.0",
159161
"chalk": "^4.1.2",
160162
"comment-json": "^4.2.3",
161163
"copyfiles": "2.4.1",

pnpm-lock.yaml

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/a11y/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/media
2+
/media-gif
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const mediaSlug = 'media'
4+
5+
export const MediaCollection: CollectionConfig = {
6+
slug: mediaSlug,
7+
access: {
8+
create: () => true,
9+
read: () => true,
10+
},
11+
fields: [],
12+
upload: {
13+
crop: true,
14+
focalPoint: true,
15+
imageSizes: [
16+
{
17+
name: 'thumbnail',
18+
height: 200,
19+
width: 200,
20+
},
21+
{
22+
name: 'medium',
23+
height: 800,
24+
width: 800,
25+
},
26+
{
27+
name: 'large',
28+
height: 1200,
29+
width: 1200,
30+
},
31+
],
32+
},
33+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const postsSlug = 'posts'
4+
5+
export const PostsCollection: CollectionConfig = {
6+
slug: postsSlug,
7+
admin: {
8+
useAsTitle: 'title',
9+
enableListViewSelectAPI: true,
10+
},
11+
fields: [
12+
{
13+
name: 'title',
14+
type: 'text',
15+
},
16+
{
17+
name: 'subtitle',
18+
type: 'text',
19+
admin: {
20+
description:
21+
'A subtitle field to test focus indicators in the admin UI, helps us detect exiting out of rich text editor properly.',
22+
},
23+
},
24+
],
25+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use client'
2+
3+
import { Button } from '@payloadcms/ui'
4+
import React from 'react'
5+
6+
import './styles.css'
7+
8+
export const FocusIndicatorsView = () => {
9+
return (
10+
<div className="focus-indicators-test-page">
11+
<h1>Focus Indicators Test Page</h1>
12+
<p>This page tests various interactive elements with different focus indicator states.</p>
13+
14+
{/* Section 1: Good focus indicators (built-in Payload components) */}
15+
<section className="test-section" data-testid="section-good-payload">
16+
<h2>Good Focus Indicators (Payload Components)</h2>
17+
<div className="button-group">
18+
<Button id="payload-button-1">Payload Button 1</Button>
19+
<Button buttonStyle="secondary" id="payload-button-2">
20+
Payload Button 2
21+
</Button>
22+
<Button buttonStyle="icon-label" icon="plus" id="payload-button-3">
23+
Add Item
24+
</Button>
25+
</div>
26+
</section>
27+
28+
{/* Section 2: Standard HTML with good focus indicators */}
29+
<section className="test-section" data-testid="section-good-html">
30+
<h2>Good Focus Indicators (Standard HTML)</h2>
31+
<div className="button-group">
32+
<button className="good-focus" id="good-button-1" type="button">
33+
Good Button 1
34+
</button>
35+
<button className="good-focus-outline" id="good-button-2" type="button">
36+
Good Button 2 (Outline)
37+
</button>
38+
<button className="good-focus-shadow" id="good-button-3" type="button">
39+
Good Button 3 (Shadow)
40+
</button>
41+
</div>
42+
<div className="link-group">
43+
<a className="good-focus" href="#section1" id="good-link-1">
44+
Good Link 1
45+
</a>
46+
<a className="good-focus-outline" href="#section2" id="good-link-2">
47+
Good Link 2
48+
</a>
49+
</div>
50+
</section>
51+
52+
{/* Section 3: Elements with focus indicators on pseudo-elements */}
53+
<section className="test-section" data-testid="section-pseudo">
54+
<h2>Focus Indicators via Pseudo-elements</h2>
55+
<div className="button-group">
56+
<button className="focus-after-outline" id="pseudo-after-outline" type="button">
57+
After Outline
58+
</button>
59+
<button className="focus-before-border" id="pseudo-before-border" type="button">
60+
Before Border
61+
</button>
62+
<button className="focus-after-shadow" id="pseudo-after-shadow" type="button">
63+
After Shadow
64+
</button>
65+
</div>
66+
</section>
67+
68+
{/* Section 4: BAD - No focus indicators */}
69+
<section className="test-section" data-testid="section-bad">
70+
{/* eslint-disable-next-line jsx-a11y/accessible-emoji */}
71+
<h2>⚠️ Bad Focus Indicators (Should Fail)</h2>
72+
<div className="button-group">
73+
<button className="no-focus" id="bad-button-1" type="button">
74+
No Focus 1
75+
</button>
76+
<button className="no-focus" id="bad-button-2" type="button">
77+
No Focus 2
78+
</button>
79+
<button className="transparent-focus" id="bad-button-3" type="button">
80+
Transparent Focus
81+
</button>
82+
</div>
83+
<div className="link-group">
84+
<a className="no-focus" href="#bad1" id="bad-link-1">
85+
Bad Link 1
86+
</a>
87+
<a className="no-focus" href="#bad2" id="bad-link-2">
88+
Bad Link 2
89+
</a>
90+
</div>
91+
<input
92+
className="no-focus"
93+
id="bad-input-1"
94+
placeholder="Input without focus indicator"
95+
type="text"
96+
/>
97+
</section>
98+
99+
{/* Section 5: Mixed - Some good, some bad */}
100+
<section className="test-section" data-testid="section-mixed">
101+
<h2>Mixed Focus Indicators</h2>
102+
<div className="form-group">
103+
<label htmlFor="good-input-1">
104+
Good Input:
105+
<input className="good-focus" id="good-input-1" placeholder="Good focus" type="text" />
106+
</label>
107+
<label htmlFor="bad-input-2">
108+
Bad Input:
109+
<input className="no-focus" id="bad-input-2" placeholder="No focus" type="text" />
110+
</label>
111+
<label htmlFor="good-select-1">
112+
Good Select:
113+
<select className="good-focus" id="good-select-1">
114+
<option>Option 1</option>
115+
<option>Option 2</option>
116+
</select>
117+
</label>
118+
<label htmlFor="bad-select-1">
119+
Bad Select:
120+
<select className="no-focus" id="bad-select-1">
121+
<option>Option 1</option>
122+
<option>Option 2</option>
123+
</select>
124+
</label>
125+
</div>
126+
</section>
127+
128+
{/* Section 6: Edge cases */}
129+
<section className="test-section" data-testid="section-edge-cases">
130+
<h2>Edge Cases</h2>
131+
<div className="button-group">
132+
<button className="zero-width-border" id="zero-width-border" type="button">
133+
Zero Width Border
134+
</button>
135+
<button className="zero-opacity-shadow" id="zero-opacity-shadow" type="button">
136+
Zero Opacity Shadow
137+
</button>
138+
<button className="transparent-outline" id="transparent-outline" type="button">
139+
Transparent Outline
140+
</button>
141+
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
142+
<div className="good-focus" id="focusable-div" tabIndex={0}>
143+
Focusable Div
144+
</div>
145+
</div>
146+
</section>
147+
148+
{/* Section 7: Disabled elements (should not be in tab order) */}
149+
<section className="test-section" data-testid="section-disabled">
150+
<h2>Disabled Elements (Not in Tab Order)</h2>
151+
<div className="button-group">
152+
<button disabled id="disabled-button" type="button">
153+
Disabled Button
154+
</button>
155+
<input disabled id="disabled-input" placeholder="Disabled input" type="text" />
156+
</div>
157+
</section>
158+
</div>
159+
)
160+
}

0 commit comments

Comments
 (0)