From dbeeeaf4e3d2b6574a6f28a16646e368de4eb8e4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 07:17:55 +0000 Subject: [PATCH] feat(ux): add keyboard focus trap to Shader Matrix modal - Implements a `keydown` listener on the modal to intercept Tab and Shift+Tab key presses. - Dynamically queries visible focusable elements (`button`, `input`, etc.) that have a valid `offsetParent`. - Loops focus between the first and last focusable elements to prevent keyboard users from tabbing out of the modal into obscured content. - Enhances accessibility for screen reader and keyboard-only users. --- .Jules/palette.md | 4 ++++ web/matrix_button.js | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/.Jules/palette.md b/.Jules/palette.md index 8d6b0da..809a956 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -9,3 +9,7 @@ ## 2025-05-24 - In-Page Navigation Focus Management **Learning:** In single-page documentation modals (common in custom nodes), in-page navigation buttons (`scrollToSection`) often scroll content but leave focus on the button. This forces keyboard users to traverse the entire menu again to reach the content. **Action:** Update scroll handlers to programmatically move focus to the target section's heading (setting `tabindex="-1"` if needed) to maintain logical reading flow. + +## 2025-05-25 - Modal Focus Trapping +**Learning:** Custom DOM modals often lack keyboard focus trapping, allowing users to tab out of the modal into the obscured background content. This is critical for keyboard and screen reader accessibility. +**Action:** Implement a 'keydown' listener on the modal container to intercept Tab/Shift+Tab. Dynamically query visible focusable elements (`offsetParent !== null`) to handle changing content (like tabs) and loop focus between the first and last elements. diff --git a/web/matrix_button.js b/web/matrix_button.js index 3224cdc..4a3935b 100644 --- a/web/matrix_button.js +++ b/web/matrix_button.js @@ -3167,6 +3167,33 @@ def apply_color_to_noise(noise_tensor, shader_params): modal.appendChild(content); document.body.appendChild(modal); + // Accessibility: Focus Trap & Initial Focus + const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + + modal.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + const focusableElements = Array.from(modal.querySelectorAll(focusableSelectors)) + .filter(el => el.offsetParent !== null && !el.hasAttribute('disabled')); // Only visible and enabled elements + + if (focusableElements.length === 0) return; + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + if (e.shiftKey) { // Shift + Tab + if (document.activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } + } else { // Tab + if (document.activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + } + }); + // Accessibility: Set focus to the close button when modal opens // Using a small timeout to ensure DOM insertion is complete and to play nice with screen readers setTimeout(() => {