From e41dd5571678349beb51d6ef15186915ee2e09ef Mon Sep 17 00:00:00 2001 From: Marcelo Paiva Date: Wed, 25 Feb 2026 15:15:41 -0500 Subject: [PATCH] Fix WCAG 2.1 AA critical and high issues: viewport zoom, focus indicators, modals, alerts, labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1: Remove maximum-scale/user-scalable=no from viewport meta (WCAG 1.4.4) - C2: Restore focus indicators on 7 inputs — replace outline-none/ring-0 with ring (WCAG 2.4.7) - C3: Add focus trap + dialog role to turbo_modal.js; focus on open, restore on close (WCAG 2.4.3, 2.1.2) - C4/C6: Replace all alert()/prompt() with ARIA live regions and custom password dialog (WCAG 3.3.1, 4.1.3) - C5: Add aria-label to signature text input, signing reason select, checkbox and radio in area.vue (WCAG 1.3.1, 4.1.2) - C7: Replace text-gray-100 → text-white on dark code blocks in _embedding.html.erb (WCAG 1.4.3) - H1: Change submission name div → h1 in submit_form/show.html.erb (WCAG 2.4.6) - H2: form.html.erb already has lang attr (confirmed correct) - H3: Add skip link to form.html.erb layout (WCAG 2.4.1) - H4: Replace text-gray-300/400 → text-gray-600 on light backgrounds across 5 files (WCAG 1.4.3) - H5: Replace close buttons → + + + + ` + + document.body.append(dialog) + + const input = dialog.querySelector(`#${inputId}`) + const cancelBtn = dialog.querySelector('[data-action="cancel"]') + const confirmBtn = dialog.querySelector('[data-action="confirm"]') + + requestAnimationFrame(() => input.focus()) + + const confirm = () => { + const passwordInput = document.createElement('input') + passwordInput.type = 'hidden' + passwordInput.name = 'password' + passwordInput.value = input.value + this.form.append(passwordInput) + dialog.remove() + this.form.requestSubmit() + this.remove() + } - input.type = 'hidden' - input.name = 'password' - input.value = prompt('Enter PDF password') + const cancel = () => { + dialog.remove() + this.remove() + } - this.form.append(input) + confirmBtn.addEventListener('click', confirm) + cancelBtn.addEventListener('click', cancel) - this.form.requestSubmit() + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') confirm() + if (e.key === 'Escape') cancel() + }) - this.remove() + dialog.addEventListener('keydown', (e) => { + if (e.key === 'Escape') cancel() + }) } get form () { diff --git a/app/javascript/elements/turbo_modal.js b/app/javascript/elements/turbo_modal.js index 69c20549..261976c5 100644 --- a/app/javascript/elements/turbo_modal.js +++ b/app/javascript/elements/turbo_modal.js @@ -1,24 +1,39 @@ import { actionable } from '@github/catalyst/lib/actionable' +const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + export default actionable(class extends HTMLElement { connectedCallback () { document.body.classList.add('overflow-hidden') - document.addEventListener('keyup', this.onEscKey) + this.setAttribute('role', 'dialog') + this.setAttribute('aria-modal', 'true') + + this._previousFocus = document.activeElement + document.addEventListener('keyup', this.onEscKey) + document.addEventListener('keydown', this.onTabKey) document.addEventListener('turbo:before-cache', this.close) if (this.dataset.closeAfterSubmit !== 'false') { document.addEventListener('turbo:submit-end', this.onSubmit) } + + requestAnimationFrame(() => { + const first = this.querySelector(FOCUSABLE) + first?.focus() + }) } disconnectedCallback () { document.body.classList.remove('overflow-hidden') document.removeEventListener('keyup', this.onEscKey) + document.removeEventListener('keydown', this.onTabKey) document.removeEventListener('turbo:submit-end', this.onSubmit) document.removeEventListener('turbo:before-cache', this.close) + + this._previousFocus?.focus() } onSubmit = (e) => { @@ -33,6 +48,28 @@ export default actionable(class extends HTMLElement { } } + onTabKey = (e) => { + if (e.key !== 'Tab') return + + const focusable = Array.from(this.querySelectorAll(FOCUSABLE)) + if (focusable.length === 0) return + + const first = focusable[0] + const last = focusable[focusable.length - 1] + + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault() + last.focus() + } + } else { + if (document.activeElement === last) { + e.preventDefault() + first.focus() + } + } + } + close = (e) => { e?.preventDefault() diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 8f323f0b..b158b564 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -134,6 +134,7 @@ v-if="submittable" type="checkbox" :value="false" + :aria-label="field.title || field.name || fieldNames[field.type]" class="aspect-square base-checkbox" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :checked="!!modelValue" @@ -154,6 +155,7 @@ v-if="submittable" type="radio" :value="false" + :aria-label="(field.title || field.name || fieldNames[field.type]) + (option?.value ? ': ' + option.value : '')" class="aspect-square checked:checkbox checked:checkbox-xs" :class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :checked="!!modelValue && modelValue === optionValue(option)" @@ -208,7 +210,7 @@ > {{ field.name }} {{ t('select_your_option') }} diff --git a/app/javascript/submission_form/phone_step.vue b/app/javascript/submission_form/phone_step.vue index b854c384..f7d22e45 100644 --- a/app/javascript/submission_form/phone_step.vue +++ b/app/javascript/submission_form/phone_step.vue @@ -102,7 +102,7 @@ ref="phone" :value="defaultValue && detectedPhoneValueDialCode ? phoneValue.split('+' + detectedPhoneValueDialCode).slice(-1).join('') : phoneValue" :readonly="!!defaultValue" - class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full" + class="base-input !text-2xl !rounded-l-none !border-l-0 w-full" autocomplete="tel" type="tel" inputmode="tel" diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index 1d1ac6d6..c77e6c70 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -205,6 +205,7 @@ + @@ -77,7 +77,7 @@
+ - +