]> danix's work - danix.xyz-2.git/commitdiff
feat: enhance modal focus trap with JavaScript and ARIA attributes
authorDanilo M. <redacted>
Fri, 17 Apr 2026 07:25:33 +0000 (09:25 +0200)
committerDanilo M. <redacted>
Fri, 17 Apr 2026 07:25:33 +0000 (09:25 +0200)
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 <redacted>
themes/danix-xyz-hacker/assets/js/form-components.js
themes/danix-xyz-hacker/layouts/partials/form-components.html

index 35a5f27980a5591634ae419bda142bae24cc13fd..ffa42605ba62a24698da2462eec93b83348bd2e4 100644 (file)
@@ -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;
index 9a69d43f98b7328aecb90a25a49fc5866f37d58c..d38973a3fef40424207faac259872922b544901d 100644 (file)
      ============================================ -->
 
 <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>
      ============================================ -->
 
 <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>
      ============================================ -->
 
 <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>