Fix WCAG 2.1 AA low-priority issues: aria-busy, keyboard nav, live regions, ARIA states

- L1: download_button.js — set aria-busy="true/false" in toggleState() during downloads (WCAG 4.1.3)
- L2: submit_form.js — announce auto-submit via data-announce-submit + announcePolite (WCAG 4.1.3)
- L3: toggle_submit.js — set aria-busy="true" on submit button when form submits (WCAG 4.1.2)
- L4: indeterminate_checkbox.js — set aria-checked="mixed" when indeterminate; update on click (WCAG 4.1.2)
- L5: review_form.js — announcePolite("Rating submitted") before auto-submit at rating 10 (WCAG 4.1.3)
- L6: masked_input.js — append sr-only hint with aria-describedby explaining masking behavior (WCAG 3.3.2)
- L7: check_on_click.js — add keydown handler for Enter/Space keys (WCAG 2.1.1)
- L9: submit_form/show.html.erb — add aria-label to icon-only download button in scroll-buttons (WCAG 4.1.2)
- L11: initials_step.vue — add :aria-label="t('minimize')" to minimize button (WCAG 4.1.2)
- L13: _drawer_events.html.erb — fix CSS typo border-base-content-/60 → border-base-content/60
- L8: app_tour.js — driver.js provides keyboard support natively (Escape/Enter); no change needed
- L10: _html_modal.html.erb — role="button" tabindex="0" aria-label already applied in Sprint 1
- L12: text-base-content/60 contrast — verified compliant at typical DaisyUI theme ratios

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/599/head
Marcelo Paiva 3 weeks ago
parent 4f0f82e967
commit 6db8b6db23

@ -1,11 +1,19 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', () => {
this.addEventListener('click', this.handleCheck)
this.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
this.handleCheck()
}
})
}
handleCheck = () => {
if (this.element && !this.element.disabled && !this.element.checked) {
this.element.checked = true
this.element.dispatchEvent(new Event('change', { bubbles: true }))
}
})
}
get element () {

@ -27,6 +27,8 @@ export default targetable(class extends HTMLElement {
toggleState () {
this.defaultButton?.classList?.toggle('hidden')
this.loadingButton?.classList?.toggle('hidden')
// aria-busy reflects whether the loading state is now active (loadingButton visible)
this.setAttribute('aria-busy', this.loadingButton?.classList?.contains('hidden') ? 'false' : 'true')
}
downloadFiles () {

@ -3,6 +3,7 @@ export default class extends HTMLElement {
if (this.dataset.indeterminate === 'true') {
this.checkbox.indeterminate = true
this.checkbox.readOnly = true
this.checkbox.setAttribute('aria-checked', 'mixed')
}
this.checkbox.addEventListener('click', () => {
@ -14,6 +15,7 @@ export default class extends HTMLElement {
if (this.checkbox.readOnly) {
this.checkbox.checked = this.checkbox.readOnly = false
this.checkbox.setAttribute('aria-checked', 'false')
} else if (!this.checkbox.checked) {
if (this.showIndeterminateEl) {
this.showIndeterminateEl.classList.remove('hidden')
@ -21,6 +23,9 @@ export default class extends HTMLElement {
this.checkbox.setAttribute('name', this.dataset.indeterminateName)
this.checkbox.checked = this.checkbox.readOnly = this.checkbox.indeterminate = true
this.checkbox.setAttribute('aria-checked', 'mixed')
} else {
this.checkbox.setAttribute('aria-checked', 'true')
}
})
}

@ -2,6 +2,16 @@ export default class extends HTMLElement {
connectedCallback () {
const maskedToken = this.input.value
const hintId = `masked-input-hint-${Math.random().toString(36).slice(2, 8)}`
const hint = document.createElement('span')
hint.id = hintId
hint.className = 'sr-only'
hint.textContent = this.dataset.maskHint || 'Value is masked. Focus to reveal.'
this.appendChild(hint)
const existing = this.input.getAttribute('aria-describedby')
this.input.setAttribute('aria-describedby', existing ? `${existing} ${hintId}` : hintId)
this.input.addEventListener('focus', () => {
this.input.value = this.dataset.token
this.input.select()

@ -1,3 +1,5 @@
import { announcePolite } from './aria_announce'
export default class extends HTMLElement {
connectedCallback () {
this.querySelectorAll('input[type="radio"]').forEach(radio => {
@ -8,6 +10,7 @@ export default class extends HTMLElement {
window.review_comment.value = ''
window.review_comment.classList.add('hidden')
window.review_submit.classList.add('hidden')
announcePolite('Rating submitted')
event.target.form.submit()
} else {
window.review_comment.classList.remove('hidden')

@ -1,3 +1,5 @@
import { announcePolite } from './aria_announce'
export default class extends HTMLElement {
connectedCallback () {
const form = this.querySelector('form') || (this.querySelector('input, button, select') || this.lastElementChild).form
@ -14,9 +16,11 @@ export default class extends HTMLElement {
if (this.dataset.submitIfValue === 'true') {
if (event.target.value) {
if (this.dataset.announceSubmit) announcePolite(this.dataset.announceSubmit)
form.requestSubmit()
}
} else {
if (this.dataset.announceSubmit) announcePolite(this.dataset.announceSubmit)
form.requestSubmit()
}
})

@ -4,6 +4,7 @@ export default class extends HTMLElement {
this.form.addEventListener('submit', () => {
this.button.disabled = true
this.button.setAttribute('aria-busy', 'true')
})
}

@ -89,6 +89,7 @@
</a>
<a
:title="t('minimize')"
:aria-label="t('minimize')"
href="#"
class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')"

@ -48,7 +48,7 @@
</span>
</label>
<% end %>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" aria-label="<%= t('download') %>">
<span data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5') %>
</span>

@ -5,7 +5,7 @@
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 %>
<li class="ml-7">
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content/60 text-base-content/60 bg-base-100" style="left: -12px;">
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
</span>
<p class="leading-none text-base-content/90 pt-1">

Loading…
Cancel
Save