a11y(loyalty): Phase 4.2 — accessibility audit fixes

Fix 15 accessibility issues across loyalty templates:

Modals (4 fixes):
- storefront/dashboard.html: barcode modal — add role="dialog",
  aria-modal, aria-labelledby, @keydown.escape
- storefront/enroll.html: terms modal — add role="dialog",
  aria-modal, aria-labelledby, aria-label on close button
- store/enroll.html: success modal — add role="dialog",
  aria-modal, aria-labelledby, @keydown.escape
- store/terminal.html: PIN entry — add aria-live="polite" on
  digit display with role="status" for screen reader announcements

Icon-only buttons (10 fixes):
- shared/pins-list.html: edit, delete, unlock — add aria-label
- admin/programs.html: view, edit, delete, activate/deactivate —
  add aria-label (dynamic for toggle state)
- store/terminal.html: clear customer, backspace — add aria-label

All buttons also get explicit type="button" where missing.

342 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 20:14:03 +02:00
parent fde58bea06
commit 24219e4d9a
6 changed files with 29 additions and 21 deletions

View File

@@ -215,7 +215,7 @@
<a
:href="'/admin/loyalty/merchants/' + program.merchant_id"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="{{ _('loyalty.common.view') }}"
aria-label="{{ _('loyalty.common.view') }}"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</a>
@@ -224,26 +224,26 @@
<a
:href="'/admin/loyalty/merchants/' + program.merchant_id + '/program'"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="{{ _('loyalty.common.edit') }}"
aria-label="{{ _('loyalty.common.edit') }}"
>
<span x-html="$icon('edit', 'w-5 h-5')"></span>
</a>
<!-- Delete Button -->
<button
<button type="button"
@click="confirmDeleteProgram(program)"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="{{ _('loyalty.common.delete') }}"
aria-label="{{ _('loyalty.common.delete') }}"
>
<span x-html="$icon('delete', 'w-5 h-5')"></span>
</button>
<!-- Activate/Deactivate Toggle -->
<button
<button type="button"
@click="toggleProgramActive(program)"
class="flex items-center justify-center p-2 rounded-lg focus:outline-none transition-colors"
:class="program.is_active ? 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-gray-700' : 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-gray-700'"
:title="program.is_active ? 'Deactivate program' : 'Activate program'"
:aria-label="program.is_active ? '{{ _('loyalty.common.deactivate') }}' : '{{ _('loyalty.common.activate') }}'"
>
<span x-html="$icon(program.is_active ? 'pause' : 'play', 'w-5 h-5')"></span>
</button>

View File

@@ -103,17 +103,19 @@
<td class="px-4 py-3">
{% if show_crud %}
<div class="flex items-center gap-2">
<button @click="openEditModal(pin)"
<button @click="openEditModal(pin)" type="button"
aria-label="{{ _('loyalty.common.edit') }}"
class="text-purple-600 hover:text-purple-700 dark:text-purple-400 text-sm">
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button>
<button @click="openDeleteModal(pin)"
<button @click="openDeleteModal(pin)" type="button"
aria-label="{{ _('loyalty.common.delete') }}"
class="text-red-600 hover:text-red-700 dark:text-red-400 text-sm">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
<button x-show="pin.is_locked" @click="unlockPin(pin)"
class="text-orange-600 hover:text-orange-700 dark:text-orange-400 text-sm"
:title="$t('loyalty.shared.pins.unlock')">
<button x-show="pin.is_locked" @click="unlockPin(pin)" type="button"
aria-label="{{ _('loyalty.shared.pins.unlock') }}"
class="text-orange-600 hover:text-orange-700 dark:text-orange-400 text-sm">
<span x-html="$icon('lock-open', 'w-4 h-4')"></span>
</button>
</div>

View File

@@ -114,13 +114,16 @@
</form>
<!-- Success Modal --> {# noqa: FE-004 #}
<div x-show="enrolledCard" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div x-show="enrolledCard"
role="dialog" aria-modal="true" aria-labelledby="enroll-success-title"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@keydown.escape.window="enrolledCard = null">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<div class="text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center">
<span x-html="$icon('check', 'w-8 h-8 text-green-500')"></span>
</div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">{{ _('loyalty.store.enroll.customer_enrolled') }}</h3>
<h3 id="enroll-success-title" class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">{{ _('loyalty.store.enroll.customer_enrolled') }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
{{ _('loyalty.store.enroll.card_number_label') }}: <span class="font-mono font-semibold" x-text="enrolledCard?.card_number"></span>
</p>

View File

@@ -131,7 +131,7 @@
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="selectedCard?.customer_email"></p>
<p class="text-xs text-gray-400 dark:text-gray-500" x-text="$t('loyalty.store.terminal.card_label') + ': ' + selectedCard?.card_number"></p>
</div>
<button @click="clearCustomer()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<button @click="clearCustomer()" type="button" aria-label="{{ _('loyalty.common.close') }}" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
@@ -324,8 +324,8 @@
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{{ _('loyalty.store.terminal.pin_authorize_text') }}
</p>
<div class="flex justify-center mb-4">
<div class="flex gap-2">
<div class="flex justify-center mb-4" aria-live="polite" aria-atomic="true">
<div class="flex gap-2" role="status" :aria-label="pinDigits.length + ' of 4 digits entered'">
<template x-for="i in 4">
<div class="w-12 h-12 border-2 rounded-lg flex items-center justify-center text-2xl font-bold"
:class="pinDigits.length >= i ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20' : 'border-gray-300 dark:border-gray-600'"
@@ -348,7 +348,7 @@
class="h-14 text-xl font-semibold rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600">
0
</button>
<button @click="removePinDigit()"
<button @click="removePinDigit()" type="button" aria-label="{{ _('loyalty.common.back') }}"
class="h-14 text-sm font-medium rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600">
<span x-html="$icon('backspace', 'w-6 h-6 mx-auto')"></span>
</button>

View File

@@ -186,10 +186,12 @@
<!-- Barcode Modal -->
<div x-show="showBarcode" x-cloak
role="dialog" aria-modal="true" aria-labelledby="barcode-modal-title"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="showBarcode = false">
@click.self="showBarcode = false"
@keydown.escape.window="showBarcode = false">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl max-w-sm w-full p-6 text-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _('loyalty.storefront.dashboard.your_loyalty_card') }}</h3>
<h3 id="barcode-modal-title" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _('loyalty.storefront.dashboard.your_loyalty_card') }}</h3>
<!-- Barcode Placeholder -->
<div class="bg-white p-4 rounded-lg mb-4">

View File

@@ -142,13 +142,14 @@
(2) create a dedicated T&C page within the loyalty module. Decision pending. #}
<!-- Terms & Conditions Modal -->
<div x-show="showTerms" x-cloak
role="dialog" aria-modal="true" aria-labelledby="terms-modal-title"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
@click.self="showTerms = false"
@keydown.escape.window="showTerms = false">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.form.terms') }}</h3>
<button @click="showTerms = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<h3 id="terms-modal-title" class="text-lg font-semibold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.form.terms') }}</h3>
<button @click="showTerms = false" type="button" aria-label="{{ _('loyalty.enrollment.close') }}" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span x-html="$icon('x-mark', 'w-5 h-5')"></span>
</button>
</div>