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.
This commit is contained in:
google-labs-jules[bot]
2026-01-22 07:17:55 +00:00
parent 6c46a39a45
commit dbeeeaf4e3
2 changed files with 31 additions and 0 deletions
+4
View File
@@ -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.
+27
View File
@@ -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(() => {