fix(structure): Rename assets and inc folders for Linux compatibility
- assets → Assets - inc → Inc Completes the case-sensitivity fixes for Linux servers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
545
Assets/js/accessibility.js
Normal file
545
Assets/js/accessibility.js
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* Accessibility JavaScript
|
||||
*
|
||||
* Mejoras de accesibilidad para navegación por teclado, gestión de focus,
|
||||
* y cumplimiento de WCAG 2.1 Level AA.
|
||||
*
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Inicializar todas las funciones de accesibilidad
|
||||
*/
|
||||
function init() {
|
||||
setupSkipLinks();
|
||||
setupKeyboardNavigation();
|
||||
setupFocusManagement();
|
||||
setupAriaLiveRegions();
|
||||
announcePageChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip Links - Navegación rápida al contenido principal
|
||||
* Permite a usuarios de teclado y lectores de pantalla saltar directamente al contenido
|
||||
*/
|
||||
function setupSkipLinks() {
|
||||
const skipLinks = document.querySelectorAll('.skip-link');
|
||||
|
||||
skipLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
e.preventDefault();
|
||||
|
||||
// Hacer el elemento focusable temporalmente
|
||||
targetElement.setAttribute('tabindex', '-1');
|
||||
|
||||
// Enfocar el elemento
|
||||
targetElement.focus();
|
||||
|
||||
// Scroll al elemento si está fuera de vista
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
// Remover tabindex después del focus para no interferir con navegación normal
|
||||
targetElement.addEventListener('blur', function() {
|
||||
targetElement.removeAttribute('tabindex');
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navegación por Teclado Mejorada
|
||||
* Maneja la navegación con teclas en menús desplegables y componentes interactivos
|
||||
*/
|
||||
function setupKeyboardNavigation() {
|
||||
// Navegación en menús desplegables de Bootstrap
|
||||
setupBootstrapDropdownKeyboard();
|
||||
|
||||
// Navegación en menús personalizados
|
||||
setupCustomMenuKeyboard();
|
||||
|
||||
// Escapar de modales con ESC
|
||||
setupModalEscape();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navegación por teclado en dropdowns de Bootstrap
|
||||
*/
|
||||
function setupBootstrapDropdownKeyboard() {
|
||||
const dropdownToggles = document.querySelectorAll('[data-bs-toggle="dropdown"]');
|
||||
|
||||
dropdownToggles.forEach(function(toggle) {
|
||||
const dropdown = toggle.nextElementSibling;
|
||||
|
||||
if (!dropdown || !dropdown.classList.contains('dropdown-menu')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Abrir dropdown con Enter o Space
|
||||
toggle.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggle.click();
|
||||
|
||||
// Focus en el primer item del menú
|
||||
setTimeout(function() {
|
||||
const firstItem = dropdown.querySelector('a, button');
|
||||
if (firstItem) {
|
||||
firstItem.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Flecha abajo para abrir y enfocar primer item
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (!dropdown.classList.contains('show')) {
|
||||
toggle.click();
|
||||
}
|
||||
setTimeout(function() {
|
||||
const firstItem = dropdown.querySelector('a, button');
|
||||
if (firstItem) {
|
||||
firstItem.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Navegación dentro del dropdown con flechas
|
||||
dropdown.addEventListener('keydown', function(e) {
|
||||
const items = Array.from(dropdown.querySelectorAll('a, button'));
|
||||
const currentIndex = items.indexOf(document.activeElement);
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const nextIndex = (currentIndex + 1) % items.length;
|
||||
items[nextIndex].focus();
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const prevIndex = currentIndex - 1 < 0 ? items.length - 1 : currentIndex - 1;
|
||||
items[prevIndex].focus();
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
toggle.click(); // Cerrar dropdown
|
||||
toggle.focus(); // Devolver focus al toggle
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
// Permitir tab normal pero cerrar dropdown
|
||||
toggle.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navegación por teclado en menús personalizados (WordPress)
|
||||
*/
|
||||
function setupCustomMenuKeyboard() {
|
||||
const menuItems = document.querySelectorAll('.menu-item-has-children > a');
|
||||
|
||||
menuItems.forEach(function(link) {
|
||||
const parentItem = link.parentElement;
|
||||
const submenu = parentItem.querySelector('.sub-menu');
|
||||
|
||||
if (!submenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inicializar ARIA attributes
|
||||
link.setAttribute('aria-haspopup', 'true');
|
||||
link.setAttribute('aria-expanded', 'false');
|
||||
submenu.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Toggle submenu con Enter o Space
|
||||
link.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleSubmenu(parentItem, submenu, link);
|
||||
}
|
||||
|
||||
// Flecha derecha para abrir submenu
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
openSubmenu(parentItem, submenu, link);
|
||||
|
||||
// Focus en primer item del submenu
|
||||
const firstItem = submenu.querySelector('a');
|
||||
if (firstItem) {
|
||||
firstItem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Escape para cerrar submenu
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSubmenu(parentItem, submenu, link);
|
||||
link.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Navegación dentro del submenu
|
||||
const submenuItems = submenu.querySelectorAll('a');
|
||||
submenuItems.forEach(function(item, index) {
|
||||
item.addEventListener('keydown', function(e) {
|
||||
// Flecha arriba/abajo para navegar entre items
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const nextItem = submenuItems[index + 1];
|
||||
if (nextItem) {
|
||||
nextItem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (index === 0) {
|
||||
link.focus();
|
||||
} else {
|
||||
submenuItems[index - 1].focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Flecha izquierda para volver al menú padre
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
closeSubmenu(parentItem, submenu, link);
|
||||
link.focus();
|
||||
}
|
||||
|
||||
// Escape para cerrar submenu
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSubmenu(parentItem, submenu, link);
|
||||
link.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cerrar submenu cuando focus sale del elemento
|
||||
parentItem.addEventListener('focusout', function(e) {
|
||||
// Verificar si el nuevo focus está fuera del menú
|
||||
setTimeout(function() {
|
||||
if (!parentItem.contains(document.activeElement)) {
|
||||
closeSubmenu(parentItem, submenu, link);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle submenu (abrir/cerrar)
|
||||
*/
|
||||
function toggleSubmenu(parentItem, submenu, link) {
|
||||
const isOpen = parentItem.classList.contains('submenu-open');
|
||||
|
||||
if (isOpen) {
|
||||
closeSubmenu(parentItem, submenu, link);
|
||||
} else {
|
||||
openSubmenu(parentItem, submenu, link);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abrir submenu
|
||||
*/
|
||||
function openSubmenu(parentItem, submenu, link) {
|
||||
parentItem.classList.add('submenu-open');
|
||||
link.setAttribute('aria-expanded', 'true');
|
||||
submenu.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar submenu
|
||||
*/
|
||||
function closeSubmenu(parentItem, submenu, link) {
|
||||
parentItem.classList.remove('submenu-open');
|
||||
link.setAttribute('aria-expanded', 'false');
|
||||
submenu.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar modales con tecla Escape
|
||||
*/
|
||||
function setupModalEscape() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
// Bootstrap modals
|
||||
const openModals = document.querySelectorAll('.modal.show');
|
||||
openModals.forEach(function(modal) {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
if (bsModal) {
|
||||
bsModal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Offcanvas
|
||||
const openOffcanvas = document.querySelectorAll('.offcanvas.show');
|
||||
openOffcanvas.forEach(function(offcanvas) {
|
||||
const bsOffcanvas = bootstrap.Offcanvas.getInstance(offcanvas);
|
||||
if (bsOffcanvas) {
|
||||
bsOffcanvas.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestión de Focus
|
||||
* Maneja el focus visible y trap de focus en modales
|
||||
*/
|
||||
function setupFocusManagement() {
|
||||
// Mostrar outline solo con navegación por teclado
|
||||
setupFocusVisible();
|
||||
|
||||
// Trap focus en modales
|
||||
setupModalFocusTrap();
|
||||
|
||||
// Restaurar focus al cerrar modales
|
||||
setupFocusRestore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus visible solo con teclado (no con mouse)
|
||||
*/
|
||||
function setupFocusVisible() {
|
||||
let usingMouse = false;
|
||||
|
||||
document.addEventListener('mousedown', function() {
|
||||
usingMouse = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab') {
|
||||
usingMouse = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Agregar clase al body para indicar método de navegación
|
||||
document.addEventListener('focusin', function() {
|
||||
if (usingMouse) {
|
||||
document.body.classList.add('using-mouse');
|
||||
} else {
|
||||
document.body.classList.remove('using-mouse');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trap focus dentro de modales (evitar que Tab salga del modal)
|
||||
*/
|
||||
function setupModalFocusTrap() {
|
||||
const modals = document.querySelectorAll('.modal, [role="dialog"]');
|
||||
|
||||
modals.forEach(function(modal) {
|
||||
modal.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Tab') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Solo aplicar trap si el modal está visible
|
||||
if (!modal.classList.contains('show') && modal.style.display !== 'block') {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusableElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab - navegar hacia atrás
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab - navegar hacia adelante
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaurar focus al elemento que abrió el modal
|
||||
*/
|
||||
function setupFocusRestore() {
|
||||
let lastFocusedElement = null;
|
||||
|
||||
// Guardar elemento con focus antes de abrir modal
|
||||
document.addEventListener('show.bs.modal', function(e) {
|
||||
lastFocusedElement = document.activeElement;
|
||||
});
|
||||
|
||||
document.addEventListener('show.bs.offcanvas', function(e) {
|
||||
lastFocusedElement = document.activeElement;
|
||||
});
|
||||
|
||||
// Restaurar focus al cerrar modal
|
||||
document.addEventListener('hidden.bs.modal', function(e) {
|
||||
if (lastFocusedElement) {
|
||||
lastFocusedElement.focus();
|
||||
lastFocusedElement = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('hidden.bs.offcanvas', function(e) {
|
||||
if (lastFocusedElement) {
|
||||
lastFocusedElement.focus();
|
||||
lastFocusedElement = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ARIA Live Regions
|
||||
* Crear regiones live para anuncios dinámicos a lectores de pantalla
|
||||
*/
|
||||
function setupAriaLiveRegions() {
|
||||
// Crear región live polite si no existe
|
||||
if (!document.getElementById('aria-live-polite')) {
|
||||
const liveRegion = document.createElement('div');
|
||||
liveRegion.id = 'aria-live-polite';
|
||||
liveRegion.setAttribute('aria-live', 'polite');
|
||||
liveRegion.setAttribute('aria-atomic', 'true');
|
||||
liveRegion.className = 'sr-only';
|
||||
document.body.appendChild(liveRegion);
|
||||
}
|
||||
|
||||
// Crear región live assertive si no existe
|
||||
if (!document.getElementById('aria-live-assertive')) {
|
||||
const liveRegion = document.createElement('div');
|
||||
liveRegion.id = 'aria-live-assertive';
|
||||
liveRegion.setAttribute('aria-live', 'assertive');
|
||||
liveRegion.setAttribute('aria-atomic', 'true');
|
||||
liveRegion.className = 'sr-only';
|
||||
document.body.appendChild(liveRegion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anunciar cambios de página a lectores de pantalla
|
||||
*/
|
||||
function announcePageChanges() {
|
||||
// Anunciar cambios de ruta en navegación (para SPAs o AJAX)
|
||||
let currentPath = window.location.pathname;
|
||||
|
||||
// Observer para cambios en el título de la página
|
||||
const titleObserver = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList') {
|
||||
const newTitle = document.title;
|
||||
announce('Página cargada: ' + newTitle, 'polite');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const titleElement = document.querySelector('title');
|
||||
if (titleElement) {
|
||||
titleObserver.observe(titleElement, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// Detectar navegación por history API
|
||||
const originalPushState = history.pushState;
|
||||
history.pushState = function() {
|
||||
originalPushState.apply(history, arguments);
|
||||
|
||||
if (window.location.pathname !== currentPath) {
|
||||
currentPath = window.location.pathname;
|
||||
setTimeout(function() {
|
||||
announce('Página cargada: ' + document.title, 'polite');
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Función helper para anunciar mensajes a lectores de pantalla
|
||||
*
|
||||
* @param {string} message - Mensaje a anunciar
|
||||
* @param {string} priority - 'polite' o 'assertive'
|
||||
*/
|
||||
window.announceToScreenReader = function(message, priority) {
|
||||
announce(message, priority);
|
||||
};
|
||||
|
||||
function announce(message, priority) {
|
||||
priority = priority || 'polite';
|
||||
const liveRegionId = 'aria-live-' + priority;
|
||||
const liveRegion = document.getElementById(liveRegionId);
|
||||
|
||||
if (!liveRegion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limpiar y agregar mensaje
|
||||
liveRegion.textContent = '';
|
||||
setTimeout(function() {
|
||||
liveRegion.textContent = message;
|
||||
}, 100);
|
||||
|
||||
// Limpiar después de 5 segundos
|
||||
setTimeout(function() {
|
||||
liveRegion.textContent = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manejar clicks en elementos con data-announce
|
||||
* Permite anunciar acciones a lectores de pantalla
|
||||
*/
|
||||
function setupDataAnnounce() {
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('[data-announce]');
|
||||
if (target) {
|
||||
const message = target.getAttribute('data-announce');
|
||||
const priority = target.getAttribute('data-announce-priority') || 'polite';
|
||||
announce(message, priority);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializar cuando DOM está listo
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
init();
|
||||
setupDataAnnounce();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
setupDataAnnounce();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user