Skip to content

Commit bfdf6da

Browse files
committed
Add role=region label to uppy Dashboard to fix ARIA violation
1 parent 50ff6d5 commit bfdf6da

6 files changed

Lines changed: 88 additions & 72 deletions

File tree

axe-results-incomplete.json

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +0,0 @@
1-
[
2-
{
3-
"id": "aria-valid-attr-value",
4-
"impact": "critical",
5-
"tags": [
6-
"cat.aria",
7-
"wcag2a",
8-
"wcag412",
9-
"EN-301-549",
10-
"EN-9.4.1.2",
11-
"RGAAv4",
12-
"RGAA-7.1.1"
13-
],
14-
"description": "Ensure all ARIA attributes have valid values",
15-
"help": "ARIA attributes must conform to valid values",
16-
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr-value?application=playwright",
17-
"nodes": [
18-
{
19-
"any": [],
20-
"all": [
21-
{
22-
"id": "aria-valid-attr-value",
23-
"data": {
24-
"messageKey": "controlsWithinPopup",
25-
"needsReview": "aria-controls=\"mastodon-modal\""
26-
},
27-
"relatedNodes": [],
28-
"impact": "critical",
29-
"message": "Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: aria-controls=\"mastodon-modal\""
30-
}
31-
],
32-
"none": [],
33-
"impact": "critical",
34-
"html": "<button type=\"button\" class=\"_button_1qagh_1 soci...\" aria-label=\"Share on Mastodon\" aria-haspopup=\"dialog\" aria-controls=\"mastodon-modal\" data-platform=\"mastodon\" data-share=\"mastodon\" data-share-text=\"Demo Page - This is ...\">",
35-
"target": [
36-
"social-share-element[url=\"/articles/demo\"] > .social-share.max-sm\\:justify-center[aria-label=\"Share this content\"] > .bg-mastodon.hover\\:bg-mastodon-active[aria-haspopup=\"dialog\"]"
37-
],
38-
"failureSummary": "Fix all of the following:\n Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: aria-controls=\"mastodon-modal\""
39-
},
40-
{
41-
"any": [],
42-
"all": [
43-
{
44-
"id": "aria-valid-attr-value",
45-
"data": {
46-
"messageKey": "controlsWithinPopup",
47-
"needsReview": "aria-controls=\"mastodon-modal\""
48-
},
49-
"relatedNodes": [],
50-
"impact": "critical",
51-
"message": "Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: aria-controls=\"mastodon-modal\""
52-
}
53-
],
54-
"none": [],
55-
"impact": "critical",
56-
"html": "<button type=\"button\" class=\"_button_1qagh_1 soci...\" aria-label=\"Share on Mastodon\" aria-haspopup=\"dialog\" aria-controls=\"mastodon-modal\" data-platform=\"mastodon\" data-share=\"mastodon\" data-share-text=\"Demo Article - Markd...\">",
57-
"target": [
58-
".gap-6.flex-col.flex > .mx-6.md\\:items-center.md\\:flex-row > .sm\\:pl-12.md\\:flex-1.w-full > social-share-element > .social-share.max-sm\\:justify-center[aria-label=\"Share this content\"] > .bg-mastodon.hover\\:bg-mastodon-active[aria-haspopup=\"dialog\"]"
59-
],
60-
"failureSummary": "Fix all of the following:\n Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: aria-controls=\"mastodon-modal\""
61-
}
62-
]
63-
}
64-
]

axe-results-violations.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
[]

src/components/Carousel/client/__tests__/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ describe('Carousel component (server output)', () => {
210210
expect(heading).toBeNull()
211211
expect(region?.getAttribute('aria-labelledby')).toBeNull()
212212
},
213-
{ title: undefined, variant: 'suggested', currentSlug: undefined }
213+
{ title: '', variant: 'suggested' }
214214
)
215215
})
216216

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { ensureUppyGeneratedAccessibility } from '@components/Pages/Contact/client/upload'
3+
import { withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
4+
5+
const flushMutations = async () => {
6+
await Promise.resolve()
7+
await Promise.resolve()
8+
}
9+
10+
describe('Contact upload accessibility', () => {
11+
it('adds a landmark role to the generated Uppy dashboard when it has an aria-label', async () => {
12+
await withJsdomEnvironment(async ({ window }) => {
13+
Object.assign(globalThis, { MutationObserver: window.MutationObserver })
14+
15+
const root = window.document.createElement('div')
16+
root.innerHTML = `
17+
<div
18+
class="uppy-Dashboard"
19+
aria-label="Uppy Dashboard"
20+
aria-hidden="false"
21+
aria-disabled="false"
22+
></div>
23+
`
24+
25+
const observer = ensureUppyGeneratedAccessibility(root)
26+
const dashboard = root.querySelector('.uppy-Dashboard')
27+
28+
expect(dashboard?.getAttribute('role')).toBe('region')
29+
30+
observer.disconnect()
31+
})
32+
})
33+
34+
it('patches dynamically added Uppy dashboard markup and unlabeled text inputs', async () => {
35+
await withJsdomEnvironment(async ({ window }) => {
36+
Object.assign(globalThis, { MutationObserver: window.MutationObserver })
37+
38+
const root = window.document.createElement('div')
39+
const observer = ensureUppyGeneratedAccessibility(root)
40+
41+
const wrapper = window.document.createElement('div')
42+
wrapper.innerHTML = `
43+
<div class="uppy-Dashboard" aria-label="Uppy Dashboard"></div>
44+
<input type="text" placeholder="Paste a remote URL" />
45+
`
46+
47+
root.append(wrapper)
48+
await flushMutations()
49+
50+
const dashboard = root.querySelector('.uppy-Dashboard')
51+
const input = root.querySelector('input[type="text"]')
52+
53+
expect(dashboard?.getAttribute('role')).toBe('region')
54+
expect(input?.getAttribute('aria-label')).toBe('Paste a remote URL')
55+
56+
observer.disconnect()
57+
})
58+
})
59+
})

src/components/Pages/Contact/client/upload.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,22 @@ const toFile = (value: Blob | File, name: string): File => {
4242

4343
const isNonNullable = <T>(value: T | null | undefined): value is T => value != null
4444

45-
const ensureUppyTextInputsAreLabeled = (root: ParentNode & Node): MutationObserver => {
45+
const UPPY_ACCESSIBILITY_FIELD_SELECTOR = 'input[type="text"], input[type="email"], textarea'
46+
const UPPY_DASHBOARD_SELECTOR = '.uppy-Dashboard'
47+
48+
const applyUppyDashboardSemantics = (element: Element): void => {
49+
if (!(element instanceof HTMLElement)) return
50+
if (!element.matches(UPPY_DASHBOARD_SELECTOR)) return
51+
if (element.hasAttribute('role')) return
52+
if (!element.getAttribute('aria-label')) return
53+
54+
element.setAttribute('role', 'region')
55+
}
56+
57+
export const ensureUppyGeneratedAccessibility = (root: ParentNode & Node): MutationObserver => {
4658
const labelIfMissing = (element: Element): void => {
4759
if (!(element instanceof HTMLElement)) return
48-
if (!element.matches('input[type="text"], input[type="email"], textarea')) return
60+
if (!element.matches(UPPY_ACCESSIBILITY_FIELD_SELECTOR)) return
4961

5062
const hasId = element.hasAttribute('id')
5163
const ariaLabel = element.getAttribute('aria-label')
@@ -61,18 +73,23 @@ const ensureUppyTextInputsAreLabeled = (root: ParentNode & Node): MutationObserv
6173
element.setAttribute('aria-label', fallback)
6274
}
6375

76+
root.querySelectorAll(UPPY_DASHBOARD_SELECTOR).forEach(applyUppyDashboardSemantics)
6477
queryAccessibilityLabelTargets(root).forEach(labelIfMissing)
6578

6679
const observer = new MutationObserver(mutations => {
6780
mutations.forEach(mutation => {
6881
mutation.addedNodes.forEach(node => {
6982
if (!(node instanceof Element)) return
70-
if (node.matches('input[type="text"], input[type="email"], textarea')) {
83+
if (node.matches(UPPY_DASHBOARD_SELECTOR)) {
84+
applyUppyDashboardSemantics(node)
85+
}
86+
87+
if (node.matches(UPPY_ACCESSIBILITY_FIELD_SELECTOR)) {
7188
labelIfMissing(node)
72-
return
7389
}
7490

7591
if ('querySelectorAll' in node) {
92+
node.querySelectorAll(UPPY_DASHBOARD_SELECTOR).forEach(applyUppyDashboardSemantics)
7693
queryAccessibilityLabelTargets(node as ParentNode).forEach(labelIfMissing)
7794
}
7895
})
@@ -124,7 +141,7 @@ export const initUppyUpload = (elements: ContactFormElements): UploadController
124141

125142
// Uppy renders several UI inputs dynamically and may also place auxiliary UI outside the
126143
// immediate dashboard target. Ensure any text/email inputs are given an accessible name.
127-
const ariaObserver = ensureUppyTextInputsAreLabeled(document)
144+
const ariaObserver = ensureUppyGeneratedAccessibility(document)
128145

129146
const reset = (): void => {
130147
uppy.cancelAll()

src/components/ThemePicker/client/__tests__/index.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,12 @@ describe('ThemePicker Component', () => {
187187
expect(tooltip.hasAttribute('hidden')).toBe(true)
188188

189189
const [firstThemeButton] = getThemeButtons(element)
190-
firstThemeButton.dispatchEvent(new window.FocusEvent('focusin', { bubbles: true }))
190+
expect(firstThemeButton).toBeTruthy()
191+
if (!firstThemeButton) {
192+
return
193+
}
194+
195+
firstThemeButton.dispatchEvent(new Event('focusin', { bubbles: true }))
191196

192197
expect(firstThemeButton.getAttribute('aria-describedby')).toBe('theme-picker-tooltip')
193198
expect(tooltip.hasAttribute('hidden')).toBe(false)

0 commit comments

Comments
 (0)