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:
@@ -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.
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user