diff options
| author | Danilo M. <danix@danix.xyz> | 2026-04-17 09:25:33 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-04-17 09:25:33 +0200 |
| commit | 7992d01ce2f196031592c50821104bedc9ca75f8 (patch) | |
| tree | 31e13fd9b2034f4fd664504b71627c9be39e557d | |
| parent | 68c5ddcbe358df8bbbc1a40b9a596c60e19c21d7 (diff) | |
| download | danixxyz-7992d01ce2f196031592c50821104bedc9ca75f8.tar.gz danixxyz-7992d01ce2f196031592c50821104bedc9ca75f8.zip | |
feat: enhance modal focus trap with JavaScript and ARIA attributes
Implements focus trap function that cycles Tab/Shift+Tab within modal boundaries,
adds ARIA attributes (role, aria-modal, aria-labelledby) for accessibility
compliance, and integrates focus initialization on modal display.
- Focus trap prevents tab escape from modal dialog
- ARIA attributes: role=dialog, aria-modal=true, aria-labelledby linking title
- Backdrop marked aria-hidden=true to exclude from accessibility tree
- Close buttons have aria-label for screen readers
- Focus initialization calls createFocusTrap on modal show
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
| -rw-r--r-- | themes/danix-xyz-hacker/assets/js/form-components.js | 36 | ||||
| -rw-r--r-- | themes/danix-xyz-hacker/layouts/partials/form-components.html | 24 |
2 files changed, 48 insertions, 12 deletions
diff --git a/themes/danix-xyz-hacker/assets/js/form-components.js b/themes/danix-xyz-hacker/assets/js/form-components.js index 35a5f27..ffa4260 100644 --- a/themes/danix-xyz-hacker/assets/js/form-components.js +++ b/themes/danix-xyz-hacker/assets/js/form-components.js @@ -89,3 +89,39 @@ export function renderToastContainer(Alpine) { } }; } + +// Focus Trap for Modals - Week 5 +function createFocusTrap(modalElement) { + const focusableElements = modalElement.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + modalElement.addEventListener('keydown', (e) => { + if (e.key !== 'Tab') return; + + if (e.shiftKey) { + // Shift + Tab + if (document.activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } + } else { + // Tab + if (document.activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + }); + + // Set initial focus + firstElement.focus(); +} + +// Export for use in Alpine.js +window.createFocusTrap = createFocusTrap; diff --git a/themes/danix-xyz-hacker/layouts/partials/form-components.html b/themes/danix-xyz-hacker/layouts/partials/form-components.html index 9a69d43..d38973a 100644 --- a/themes/danix-xyz-hacker/layouts/partials/form-components.html +++ b/themes/danix-xyz-hacker/layouts/partials/form-components.html @@ -153,11 +153,11 @@ ============================================ --> <div class="modal" :class="{ active: showAlertModal }" x-show="showAlertModal"> - <div class="modal-backdrop" @click="showAlertModal = false"></div> - <div class="modal-content modal-sm"> + <div class="modal-backdrop" @click="showAlertModal = false" aria-hidden="true"></div> + <div class="modal-content modal-sm" role="dialog" aria-labelledby="alert-modal-title" aria-modal="true" @x-show.transition.in="createFocusTrap($el)"> <div class="modal-header"> - <h3 class="modal-title">{{ i18n "form_alert_title" | default "Alert" }}</h3> - <div class="modal-close" @click="showAlertModal = false"></div> + <h3 class="modal-title" id="alert-modal-title">{{ i18n "form_alert_title" | default "Alert" }}</h3> + <div class="modal-close" @click="showAlertModal = false" aria-label="Close modal"></div> </div> <div class="modal-body"> <p>{{ i18n "form_alert_message" | default "This is an alert modal. Click OK to dismiss." }}</p> @@ -173,11 +173,11 @@ ============================================ --> <div class="modal" :class="{ active: showConfirmModal }" x-show="showConfirmModal"> - <div class="modal-backdrop" @click="showConfirmModal = false"></div> - <div class="modal-content modal-sm"> + <div class="modal-backdrop" @click="showConfirmModal = false" aria-hidden="true"></div> + <div class="modal-content modal-sm" role="dialog" aria-labelledby="confirm-modal-title" aria-modal="true" @x-show.transition.in="createFocusTrap($el)"> <div class="modal-header"> - <h3 class="modal-title">{{ i18n "form_confirm_title" | default "Confirm Action" }}</h3> - <div class="modal-close" @click="showConfirmModal = false"></div> + <h3 class="modal-title" id="confirm-modal-title">{{ i18n "form_confirm_title" | default "Confirm Action" }}</h3> + <div class="modal-close" @click="showConfirmModal = false" aria-label="Close modal"></div> </div> <div class="modal-body"> <p>{{ i18n "form_confirm_message" | default "Are you sure you want to continue?" }}</p> @@ -194,11 +194,11 @@ ============================================ --> <div class="modal" :class="{ active: showContentModal }" x-show="showContentModal"> - <div class="modal-backdrop" @click="showContentModal = false"></div> - <div class="modal-content modal-md"> + <div class="modal-backdrop" @click="showContentModal = false" aria-hidden="true"></div> + <div class="modal-content modal-md" role="dialog" aria-labelledby="content-modal-title" aria-modal="true" @x-show.transition.in="createFocusTrap($el)"> <div class="modal-header"> - <h3 class="modal-title">{{ i18n "form_content_title" | default "Modal with Content" }}</h3> - <div class="modal-close" @click="showContentModal = false"></div> + <h3 class="modal-title" id="content-modal-title">{{ i18n "form_content_title" | default "Modal with Content" }}</h3> + <div class="modal-close" @click="showContentModal = false" aria-label="Close modal"></div> </div> <div class="modal-body"> <p>{{ i18n "form_content_message" | default "This modal contains detailed content. You can add forms, lists, or any HTML here." }}</p> |
