Skip to content

Commit 4d9f4f1

Browse files
committed
Add MutationObserver to Uppy dynamically-added form inputs that adds accessible names
1 parent 6c9e53d commit 4d9f4f1

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

‎src/components/Forms/Contact/client/selectors.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,19 @@ export const queryUppyDashboardTarget = (uppyContainer: HTMLElement | null): HTM
137137
const dashboardTarget = uppyContainer.querySelector('#uppyDashboard')
138138
return dashboardTarget instanceof HTMLElement ? dashboardTarget : null
139139
}
140+
141+
export type AccessibilityLabelTarget = HTMLInputElement | HTMLTextAreaElement
142+
143+
const isAccessibilityLabelTarget = (element: Element): element is AccessibilityLabelTarget => {
144+
if (isInputElement(element)) {
145+
return element.type === 'text' || element.type === 'email'
146+
}
147+
148+
return element.tagName === 'TEXTAREA'
149+
}
150+
151+
export const queryAccessibilityLabelTargets = (root: ParentNode): AccessibilityLabelTarget[] => {
152+
return Array.from(root.querySelectorAll('input[type="text"], input[type="email"], textarea')).filter(
153+
isAccessibilityLabelTarget
154+
)
155+
}

‎src/components/Forms/Contact/client/upload.ts‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Audio from '@uppy/audio'
44
import Webcam from '@uppy/webcam'
55

66
import type { ContactFormElements } from './@types'
7-
import { queryUppyDashboardTarget } from './selectors'
7+
import { queryAccessibilityLabelTargets, queryUppyDashboardTarget } from './selectors'
88
import { isUnitTest } from '@components/scripts/utils/environmentClient'
99

1010
export interface UploadController {
@@ -42,6 +42,47 @@ 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 => {
46+
const labelIfMissing = (element: Element): void => {
47+
if (!(element instanceof HTMLElement)) return
48+
if (!element.matches('input[type="text"], input[type="email"], textarea')) return
49+
50+
const hasId = element.hasAttribute('id')
51+
const ariaLabel = element.getAttribute('aria-label')
52+
const ariaLabelledBy = element.getAttribute('aria-labelledby')
53+
if (hasId || ariaLabel || ariaLabelledBy) return
54+
55+
const placeholder =
56+
element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
57+
? element.placeholder
58+
: ''
59+
const fallback = placeholder || element.getAttribute('name') || 'File upload'
60+
61+
element.setAttribute('aria-label', fallback)
62+
}
63+
64+
queryAccessibilityLabelTargets(root).forEach(labelIfMissing)
65+
66+
const observer = new MutationObserver(mutations => {
67+
mutations.forEach(mutation => {
68+
mutation.addedNodes.forEach(node => {
69+
if (!(node instanceof Element)) return
70+
if (node.matches('input[type="text"], input[type="email"], textarea')) {
71+
labelIfMissing(node)
72+
return
73+
}
74+
75+
if ('querySelectorAll' in node) {
76+
queryAccessibilityLabelTargets(node as ParentNode).forEach(labelIfMissing)
77+
}
78+
})
79+
})
80+
})
81+
82+
observer.observe(root, { childList: true, subtree: true })
83+
return observer
84+
}
85+
4586
export const initUppyUpload = (elements: ContactFormElements): UploadController | null => {
4687
if (!elements.uppyContainer) {
4788
return null
@@ -81,6 +122,10 @@ export const initUppyUpload = (elements: ContactFormElements): UploadController
81122
.use(Audio)
82123
.use(Webcam)
83124

125+
// Uppy renders several UI inputs dynamically and may also place auxiliary UI outside the
126+
// immediate dashboard target. Ensure any text/email inputs are given an accessible name.
127+
const ariaObserver = ensureUppyTextInputsAreLabeled(document)
128+
84129
const reset = (): void => {
85130
uppy.cancelAll()
86131
const fileIds = uppy.getFiles().map(file => file.id)
@@ -100,6 +145,9 @@ export const initUppyUpload = (elements: ContactFormElements): UploadController
100145
.filter(isNonNullable)
101146
},
102147
reset,
103-
destroy: () => uppy.destroy(),
148+
destroy: () => {
149+
ariaObserver.disconnect()
150+
uppy.destroy()
151+
},
104152
}
105153
}

0 commit comments

Comments
 (0)