@@ -4,7 +4,7 @@ import Audio from '@uppy/audio'
44import Webcam from '@uppy/webcam'
55
66import type { ContactFormElements } from './@types'
7- import { queryUppyDashboardTarget } from './selectors'
7+ import { queryAccessibilityLabelTargets , queryUppyDashboardTarget } from './selectors'
88import { isUnitTest } from '@components/scripts/utils/environmentClient'
99
1010export interface UploadController {
@@ -42,6 +42,47 @@ const toFile = (value: Blob | File, name: string): File => {
4242
4343const 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+
4586export 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