I’ll build this as a safe, dependency-free accessibility helper for RX Theme: keyboard navigation, skip links, focus handling, reduced-motion support, ARIA helpers, forms, tables, menus, modals, and WordPress-friendly initialization.
Create this file:
assets/static-js/static-chunks-js/chunk-021-accessibility.js
/*!
* RX Theme Accessibility Chunk
* File: assets/static-js/static-chunks-js/chunk-021-accessibility.js
* Purpose: Advanced accessibility helpers for RX Theme
* Author: RxHarun
*
* Safe features:
* - Skip link support
* - Keyboard navigation
* - Focus-visible handling
* - Mobile menu accessibility
* - Dropdown menu accessibility
* - Modal accessibility
* - Accordion accessibility
* - Tabs accessibility
* - Form accessibility
* - Table accessibility
* - External link labels
* - Reduced motion support
* - Live region announcements
* - Lazy accessibility fixes
* - WordPress admin-bar aware focus offset
*/
(function () {
'use strict';
const RXA11Y = {
version: '1.0.0',
selectors: {
body: 'body',
siteHeader: '.site-header, #masthead, header[role="banner"]',
main: 'main, #main, .site-main, #primary',
nav: 'nav, .main-navigation, .primary-navigation',
menuToggle: '.menu-toggle, .nav-toggle, .mobile-menu-toggle, [data-rx-menu-toggle]',
menuContainer: '.menu, .nav-menu, .primary-menu, [data-rx-menu]',
dropdownParent: '.menu-item-has-children, .page_item_has_children, [data-rx-has-submenu]',
submenu: '.sub-menu, .children, [data-rx-submenu]',
searchToggle: '[data-rx-search-toggle], .search-toggle',
searchForm: '.search-form, [role="search"], [data-rx-search-form]',
modal: '[data-rx-modal], .rx-modal',
modalOpen: '[data-rx-modal-open]',
modalClose: '[data-rx-modal-close], .rx-modal-close',
accordion: '[data-rx-accordion]',
accordionButton: '[data-rx-accordion-button]',
accordionPanel: '[data-rx-accordion-panel]',
tabs: '[data-rx-tabs]',
tabList: '[role="tablist"], [data-rx-tablist]',
tab: '[role="tab"], [data-rx-tab]',
tabPanel: '[role="tabpanel"], [data-rx-tabpanel]',
table: 'table',
iframe: 'iframe',
image: 'img',
form: 'form',
input: 'input, textarea, select',
invalidInput: 'input:invalid, textarea:invalid, select:invalid',
externalLink: 'a[target="_blank"]',
skipLink: '.skip-link, .screen-reader-text[href^="#"]'
},
classNames: {
js: 'rx-js',
noMotion: 'rx-reduced-motion',
keyboardUser: 'rx-keyboard-user',
mouseUser: 'rx-mouse-user',
menuOpen: 'rx-menu-open',
submenuOpen: 'rx-submenu-open',
modalOpen: 'rx-modal-open',
focusTrapActive: 'rx-focus-trap-active',
screenReaderText: 'screen-reader-text',
visuallyHidden: 'rx-visually-hidden',
hasFocus: 'rx-has-focus',
isReady: 'rx-a11y-ready'
},
keys: {
tab: 'Tab',
enter: 'Enter',
escape: 'Escape',
space: ' ',
spaceLegacy: 'Spacebar',
arrowUp: 'ArrowUp',
arrowDown: 'ArrowDown',
arrowLeft: 'ArrowLeft',
arrowRight: 'ArrowRight',
home: 'Home',
end: 'End'
},
state: {
lastFocusedElement: null,
activeModal: null,
activeFocusTrap: null,
liveRegion: null,
reducedMotion: false,
initialized: false
}
};
const doc = document;
const win = window;
const html = doc.documentElement;
const body = doc.body;
if (!body) {
return;
}
function qs(selector, context = doc) {
return context.querySelector(selector);
}
function qsa(selector, context = doc) {
return Array.prototype.slice.call(context.querySelectorAll(selector));
}
function isElement(value) {
return value instanceof Element || value instanceof HTMLDocument;
}
function isVisible(element) {
if (!element || !isElement(element)) return false;
const style = win.getComputedStyle(element);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
) {
return false;
}
const rect = element.getBoundingClientRect();
return !!(
rect.width ||
rect.height ||
element.getClientRects().length
);
}
function isFocusable(element) {
if (!element || !isElement(element)) return false;
if (element.disabled) return false;
if (element.getAttribute('aria-hidden') === 'true') return false;
if (element.hasAttribute('hidden')) return false;
if (!isVisible(element)) return false;
const focusableSelectors = [
'a[href]',
'area[href]',
'button:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'select:not([disabled])',
'textarea:not([disabled])',
'iframe',
'object',
'embed',
'[contenteditable="true"]',
'[tabindex]:not([tabindex="-1"])'
];
return element.matches(focusableSelectors.join(','));
}
function getFocusableElements(container = doc) {
const focusableSelectors = [
'a[href]',
'area[href]',
'button:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'select:not([disabled])',
'textarea:not([disabled])',
'iframe',
'object',
'embed',
'[contenteditable="true"]',
'[tabindex]:not([tabindex="-1"])'
];
return qsa(focusableSelectors.join(','), container).filter(isFocusable);
}
function safeFocus(element, options = {}) {
if (!element || !isElement(element)) return;
try {
element.focus({
preventScroll: !!options.preventScroll
});
} catch (error) {
try {
element.focus();
} catch (innerError) {
return;
}
}
if (options.scroll) {
element.scrollIntoView({
behavior: RXA11Y.state.reducedMotion ? 'auto' : 'smooth',
block: 'center'
});
}
}
function uniqueId(prefix = 'rx-a11y') {
return `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
}
function ensureId(element, prefix = 'rx-a11y') {
if (!element.id) {
element.id = uniqueId(prefix);
}
return element.id;
}
function setExpanded(element, expanded) {
if (!element) return;
element.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
function setHidden(element, hidden) {
if (!element) return;
if (hidden) {
element.setAttribute('hidden', '');
element.setAttribute('aria-hidden', 'true');
} else {
element.removeAttribute('hidden');
element.setAttribute('aria-hidden', 'false');
}
}
function hasText(element) {
if (!element) return false;
return element.textContent && element.textContent.trim().length > 0;
}
function createScreenReaderText(text) {
const span = doc.createElement('span');
span.className = RXA11Y.classNames.screenReaderText;
span.textContent = text;
return span;
}
function announce(message, politeness = 'polite') {
if (!message) return;
if (!RXA11Y.state.liveRegion) {
const region = doc.createElement('div');
region.className = 'rx-a11y-live-region screen-reader-text';
region.setAttribute('aria-live', politeness);
region.setAttribute('aria-atomic', 'true');
body.appendChild(region);
RXA11Y.state.liveRegion = region;
}
RXA11Y.state.liveRegion.setAttribute('aria-live', politeness);
RXA11Y.state.liveRegion.textContent = '';
win.setTimeout(function () {
RXA11Y.state.liveRegion.textContent = message;
}, 50);
}
function initBaseClasses() {
html.classList.add(RXA11Y.classNames.js);
body.classList.add(RXA11Y.classNames.isReady);
}
function initReducedMotion() {
const mediaQuery = win.matchMedia('(prefers-reduced-motion: reduce)');
function updateReducedMotion() {
RXA11Y.state.reducedMotion = mediaQuery.matches;
if (mediaQuery.matches) {
html.classList.add(RXA11Y.classNames.noMotion);
} else {
html.classList.remove(RXA11Y.classNames.noMotion);
}
}
updateReducedMotion();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateReducedMotion);
} else if (typeof mediaQuery.addListener === 'function') {
mediaQuery.addListener(updateReducedMotion);
}
}
function initInputModeDetection() {
function setKeyboardMode() {
body.classList.add(RXA11Y.classNames.keyboardUser);
body.classList.remove(RXA11Y.classNames.mouseUser);
}
function setMouseMode() {
body.classList.add(RXA11Y.classNames.mouseUser);
body.classList.remove(RXA11Y.classNames.keyboardUser);
}
doc.addEventListener('keydown', function (event) {
if (event.key === RXA11Y.keys.tab) {
setKeyboardMode();
}
});
doc.addEventListener('mousedown', setMouseMode);
doc.addEventListener('pointerdown', setMouseMode);
doc.addEventListener('touchstart', setMouseMode, { passive: true });
}
function initFocusWithinPolyfill() {
doc.addEventListener('focusin', function (event) {
const target = event.target;
if (!target || !isElement(target)) return;
target.classList.add(RXA11Y.classNames.hasFocus);
let parent = target.parentElement;
while (parent && parent !== body) {
parent.classList.add(RXA11Y.classNames.hasFocus);
parent = parent.parentElement;
}
});
doc.addEventListener('focusout', function (event) {
const target = event.target;
if (!target || !isElement(target)) return;
win.setTimeout(function () {
qsa(`.${RXA11Y.classNames.hasFocus}`).forEach(function (element) {
if (!element.contains(doc.activeElement)) {
element.classList.remove(RXA11Y.classNames.hasFocus);
}
});
}, 0);
});
}
function initSkipLinks() {
let main = qs(RXA11Y.selectors.main);
if (!main) {
main = doc.createElement('main');
main.id = 'main';
main.className = 'site-main';
body.appendChild(main);
}
const mainId = ensureId(main, 'rx-main');
let existingSkipLink = qs(`a[href="#${mainId}"]`);
if (!existingSkipLink) {
existingSkipLink = doc.createElement('a');
existingSkipLink.className = 'skip-link screen-reader-text';
existingSkipLink.href = `#${mainId}`;
existingSkipLink.textContent = 'Skip to main content';
body.insertBefore(existingSkipLink, body.firstChild);
}
existingSkipLink.addEventListener('click', function (event) {
const target = qs(this.getAttribute('href'));
if (!target) return;
event.preventDefault();
if (!target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '-1');
}
safeFocus(target, {
scroll: true
});
});
}
function initLandmarks() {
const header = qs(RXA11Y.selectors.siteHeader);
const main = qs(RXA11Y.selectors.main);
const navs = qsa(RXA11Y.selectors.nav);
if (header && !header.getAttribute('role')) {
header.setAttribute('role', 'banner');
}
if (main && main.tagName.toLowerCase() !== 'main') {
main.setAttribute('role', 'main');
}
navs.forEach(function (nav, index) {
if (!nav.getAttribute('role')) {
nav.setAttribute('role', 'navigation');
}
if (!nav.getAttribute('aria-label') && !nav.getAttribute('aria-labelledby')) {
nav.setAttribute('aria-label', index === 0 ? 'Primary navigation' : `Navigation ${index + 1}`);
}
});
}
function initMobileMenu() {
const toggles = qsa(RXA11Y.selectors.menuToggle);
toggles.forEach(function (toggle) {
const targetSelector =
toggle.getAttribute('data-rx-menu-target') ||
toggle.getAttribute('aria-controls');
let menu = null;
if (targetSelector) {
menu = qs(targetSelector.startsWith('#') ? targetSelector : `#${targetSelector}`);
}
if (!menu) {
const nav = toggle.closest(RXA11Y.selectors.nav);
menu = nav ? qs(RXA11Y.selectors.menuContainer, nav) : qs(RXA11Y.selectors.menuContainer);
}
if (!menu) return;
const menuId = ensureId(menu, 'rx-menu');
toggle.setAttribute('aria-controls', menuId);
setExpanded(toggle, false);
if (!toggle.getAttribute('aria-label') && !hasText(toggle)) {
toggle.setAttribute('aria-label', 'Open navigation menu');
}
function openMenu() {
body.classList.add(RXA11Y.classNames.menuOpen);
menu.classList.add(RXA11Y.classNames.menuOpen);
setExpanded(toggle, true);
toggle.setAttribute('aria-label', 'Close navigation menu');
announce('Navigation menu opened');
}
function closeMenu(options = {}) {
body.classList.remove(RXA11Y.classNames.menuOpen);
menu.classList.remove(RXA11Y.classNames.menuOpen);
setExpanded(toggle, false);
toggle.setAttribute('aria-label', 'Open navigation menu');
if (options.focusToggle) {
safeFocus(toggle);
}
announce('Navigation menu closed');
}
function isOpen() {
return toggle.getAttribute('aria-expanded') === 'true';
}
toggle.addEventListener('click', function () {
if (isOpen()) {
closeMenu();
} else {
openMenu();
}
});
toggle.addEventListener('keydown', function (event) {
if (event.key === RXA11Y.keys.enter || event.key === RXA11Y.keys.space || event.key === RXA11Y.keys.spaceLegacy) {
event.preventDefault();
if (isOpen()) {
closeMenu();
} else {
openMenu();
}
}
if (event.key === RXA11Y.keys.escape && isOpen()) {
closeMenu({
focusToggle: true
});
}
});
doc.addEventListener('keydown', function (event) {
if (event.key === RXA11Y.keys.escape && isOpen()) {
closeMenu({
focusToggle: true
});
}
});
doc.addEventListener('click', function (event) {
if (!isOpen()) return;
if (menu.contains(event.target) || toggle.contains(event.target)) return;
closeMenu();
});
});
}
function initDropdownMenus() {
const parents = qsa(RXA11Y.selectors.dropdownParent);
parents.forEach(function (parent) {
const submenu = qs(RXA11Y.selectors.submenu, parent);
const directLink = qs(':scope > a, :scope > button', parent);
if (!submenu || !directLink) return;
const submenuId = ensureId(submenu, 'rx-submenu');
directLink.setAttribute('aria-haspopup', 'true');
directLink.setAttribute('aria-controls', submenuId);
setExpanded(directLink, false);
function openSubmenu() {
parent.classList.add(RXA11Y.classNames.submenuOpen);
setExpanded(directLink, true);
}
function closeSubmenu() {
parent.classList.remove(RXA11Y.classNames.submenuOpen);
setExpanded(directLink, false);
}
function toggleSubmenu(event) {
const expanded = directLink.getAttribute('aria-expanded') === 'true';
if (expanded) {
closeSubmenu();
} else {
openSubmenu();
}
if (event) {
event.preventDefault();
}
}
directLink.addEventListener('keydown', function (event) {
const submenuLinks = getFocusableElements(submenu);
if (event.key === RXA11Y.keys.arrowDown) {
event.preventDefault();
openSubmenu();
if (submenuLinks.length) {
safeFocus(submenuLinks[0]);
}
}
if (event.key === RXA11Y.keys.escape) {
event.preventDefault();
closeSubmenu();
safeFocus(directLink);
}
if (event.key === RXA11Y.keys.space || event.key === RXA11Y.keys.spaceLegacy) {
toggleSubmenu(event);
}
});
submenu.addEventListener('keydown', function (event) {
const links = getFocusableElements(submenu);
const currentIndex = links.indexOf(doc.activeElement);
if (event.key === RXA11Y.keys.escape) {
event.preventDefault();
closeSubmenu();
safeFocus(directLink);
}
if (event.key === RXA11Y.keys.arrowDown && links.length) {
event.preventDefault();
safeFocus(links[(currentIndex + 1) % links.length]);
}
if (event.key === RXA11Y.keys.arrowUp && links.length) {
event.preventDefault();
safeFocus(links[(currentIndex - 1 + links.length) % links.length]);
}
if (event.key === RXA11Y.keys.home && links.length) {
event.preventDefault();
safeFocus(links[0]);
}
if (event.key === RXA11Y.keys.end && links.length) {
event.preventDefault();
safeFocus(links[links.length - 1]);
}
});
parent.addEventListener('mouseenter', openSubmenu);
parent.addEventListener('mouseleave', closeSubmenu);
parent.addEventListener('focusout', function () {
win.setTimeout(function () {
if (!parent.contains(doc.activeElement)) {
closeSubmenu();
}
}, 0);
});
});
}
function trapFocus(container, event) {
const focusable = getFocusableElements(container);
if (!focusable.length) {
event.preventDefault();
safeFocus(container);
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && doc.activeElement === first) {
event.preventDefault();
safeFocus(last);
} else if (!event.shiftKey && doc.activeElement === last) {
event.preventDefault();
safeFocus(first);
}
}
function initModals() {
const openers = qsa(RXA11Y.selectors.modalOpen);
const modals = qsa(RXA11Y.selectors.modal);
modals.forEach(function (modal) {
if (!modal.getAttribute('role')) {
modal.setAttribute('role', 'dialog');
}
modal.setAttribute('aria-modal', 'true');
if (!modal.getAttribute('aria-label') && !modal.getAttribute('aria-labelledby')) {
const heading = qs('h1, h2, h3, h4, h5, h6', modal);
if (heading) {
modal.setAttribute('aria-labelledby', ensureId(heading, 'rx-modal-title'));
} else {
modal.setAttribute('aria-label', 'Dialog');
}
}
setHidden(modal, true);
});
function openModal(modal, opener) {
if (!modal) return;
RXA11Y.state.lastFocusedElement = opener || doc.activeElement;
RXA11Y.state.activeModal = modal;
setHidden(modal, false);
body.classList.add(RXA11Y.classNames.modalOpen);
const focusTarget =
qs('[data-rx-modal-focus]', modal) ||
getFocusableElements(modal)[0] ||
modal;
if (!modal.hasAttribute('tabindex')) {
modal.setAttribute('tabindex', '-1');
}
safeFocus(focusTarget);
announce('Dialog opened');
}
function closeModal(modal, options = {}) {
if (!modal) return;
setHidden(modal, true);
body.classList.remove(RXA11Y.classNames.modalOpen);
RXA11Y.state.activeModal = null;
if (options.restoreFocus !== false && RXA11Y.state.lastFocusedElement) {
safeFocus(RXA11Y.state.lastFocusedElement);
}
announce('Dialog closed');
}
openers.forEach(function (opener) {
const targetSelector =
opener.getAttribute('data-rx-modal-open') ||
opener.getAttribute('href') ||
opener.getAttribute('aria-controls');
const modal = targetSelector ? qs(targetSelector) : null;
if (!modal) return;
opener.setAttribute('aria-haspopup', 'dialog');
opener.setAttribute('aria-controls', ensureId(modal, 'rx-modal'));
opener.addEventListener('click', function (event) {
event.preventDefault();
openModal(modal, opener);
});
});
doc.addEventListener('click', function (event) {
const closeButton = event.target.closest(RXA11Y.selectors.modalClose);
if (closeButton) {
event.preventDefault();
closeModal(closeButton.closest(RXA11Y.selectors.modal));
return;
}
const activeModal = RXA11Y.state.activeModal;
if (
activeModal &&
event.target === activeModal &&
activeModal.getAttribute('data-rx-modal-static') !== 'true'
) {
closeModal(activeModal);
}
});
doc.addEventListener('keydown', function (event) {
const activeModal = RXA11Y.state.activeModal;
if (!activeModal) return;
if (event.key === RXA11Y.keys.escape && activeModal.getAttribute('data-rx-modal-esc') !== 'false') {
closeModal(activeModal);
}
if (event.key === RXA11Y.keys.tab) {
trapFocus(activeModal, event);
}
});
}
function initAccordions() {
const accordions = qsa(RXA11Y.selectors.accordion);
accordions.forEach(function (accordion) {
const buttons = qsa(RXA11Y.selectors.accordionButton, accordion);
buttons.forEach(function (button, index) {
let panel = null;
const controls = button.getAttribute('aria-controls') || button.getAttribute('data-rx-controls');
if (controls) {
panel = qs(`#${controls}`);
}
if (!panel) {
panel = qsa(RXA11Y.selectors.accordionPanel, accordion)[index];
}
if (!panel) return;
const buttonId = ensureId(button, 'rx-accordion-button');
const panelId = ensureId(panel, 'rx-accordion-panel');
button.setAttribute('aria-controls', panelId);
panel.setAttribute('aria-labelledby', buttonId);
if (!button.hasAttribute('aria-expanded')) {
setExpanded(button, false);
setHidden(panel, true);
}
button.addEventListener('click', function () {
const expanded = button.getAttribute('aria-expanded') === 'true';
const allowMultiple = accordion.getAttribute('data-rx-accordion-multiple') === 'true';
if (!allowMultiple) {
buttons.forEach(function (otherButton) {
if (otherButton === button) return;
const otherPanel = qs(`#${otherButton.getAttribute('aria-controls')}`);
setExpanded(otherButton, false);
setHidden(otherPanel, true);
});
}
setExpanded(button, !expanded);
setHidden(panel, expanded);
});
button.addEventListener('keydown', function (event) {
const currentIndex = buttons.indexOf(button);
if (event.key === RXA11Y.keys.arrowDown) {
event.preventDefault();
safeFocus(buttons[(currentIndex + 1) % buttons.length]);
}
if (event.key === RXA11Y.keys.arrowUp) {
event.preventDefault();
safeFocus(buttons[(currentIndex - 1 + buttons.length) % buttons.length]);
}
if (event.key === RXA11Y.keys.home) {
event.preventDefault();
safeFocus(buttons[0]);
}
if (event.key === RXA11Y.keys.end) {
event.preventDefault();
safeFocus(buttons[buttons.length - 1]);
}
});
});
});
}
function initTabs() {
const tabGroups = qsa(RXA11Y.selectors.tabs);
tabGroups.forEach(function (group) {
const tabList = qs(RXA11Y.selectors.tabList, group) || group;
const tabs = qsa(RXA11Y.selectors.tab, group);
const panels = qsa(RXA11Y.selectors.tabPanel, group);
if (!tabs.length || !panels.length) return;
tabList.setAttribute('role', 'tablist');
tabs.forEach(function (tab, index) {
const panel = panels[index];
if (!panel) return;
const tabId = ensureId(tab, 'rx-tab');
const panelId = ensureId(panel, 'rx-tab-panel');
tab.setAttribute('role', 'tab');
tab.setAttribute('aria-controls', panelId);
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('aria-labelledby', tabId);
const selected = index === 0;
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
tab.setAttribute('tabindex', selected ? '0' : '-1');
if (!selected) {
setHidden(panel, true);
} else {
setHidden(panel, false);
}
function activateTab(focus = true) {
tabs.forEach(function (otherTab, otherIndex) {
const otherPanel = panels[otherIndex];
const isCurrent = otherTab === tab;
otherTab.setAttribute('aria-selected', isCurrent ? 'true' : 'false');
otherTab.setAttribute('tabindex', isCurrent ? '0' : '-1');
if (otherPanel) {
setHidden(otherPanel, !isCurrent);
}
});
if (focus) {
safeFocus(tab);
}
}
tab.addEventListener('click', function (event) {
event.preventDefault();
activateTab(false);
});
tab.addEventListener('keydown', function (event) {
const currentIndex = tabs.indexOf(tab);
let nextIndex = null;
if (event.key === RXA11Y.keys.arrowRight) {
nextIndex = (currentIndex + 1) % tabs.length;
}
if (event.key === RXA11Y.keys.arrowLeft) {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
}
if (event.key === RXA11Y.keys.home) {
nextIndex = 0;
}
if (event.key === RXA11Y.keys.end) {
nextIndex = tabs.length - 1;
}
if (nextIndex !== null) {
event.preventDefault();
tabs[nextIndex].click();
safeFocus(tabs[nextIndex]);
}
});
});
});
}
function initSearchForms() {
const forms = qsa(RXA11Y.selectors.searchForm);
forms.forEach(function (form, index) {
if (!form.getAttribute('role')) {
form.setAttribute('role', 'search');
}
if (!form.getAttribute('aria-label') && !form.getAttribute('aria-labelledby')) {
form.setAttribute('aria-label', index === 0 ? 'Site search' : `Site search ${index + 1}`);
}
const input = qs('input[type="search"], input[name="s"], input[type="text"]', form);
if (input) {
if (!input.getAttribute('aria-label') && !input.getAttribute('aria-labelledby')) {
input.setAttribute('aria-label', 'Search keywords');
}
if (!input.getAttribute('autocomplete')) {
input.setAttribute('autocomplete', 'search');
}
}
});
}
function initFormAccessibility() {
const forms = qsa(RXA11Y.selectors.form);
forms.forEach(function (form) {
const inputs = qsa(RXA11Y.selectors.input, form);
inputs.forEach(function (input) {
const id = ensureId(input, 'rx-field');
const hasLabel =
qs(`label[for="${CSS.escape(id)}"]`, form) ||
input.closest('label') ||
input.getAttribute('aria-label') ||
input.getAttribute('aria-labelledby');
if (!hasLabel) {
const placeholder = input.getAttribute('placeholder');
const name = input.getAttribute('name');
if (placeholder) {
input.setAttribute('aria-label', placeholder);
} else if (name) {
input.setAttribute('aria-label', name.replace(/[-_]/g, ' '));
}
}
input.addEventListener('invalid', function () {
const message = input.validationMessage || 'Please check this field.';
input.setAttribute('aria-invalid', 'true');
let error = input.nextElementSibling;
if (!error || !error.classList.contains('rx-field-error')) {
error = doc.createElement('span');
error.className = 'rx-field-error screen-reader-text';
error.id = `${id}-error`;
input.insertAdjacentElement('afterend', error);
}
error.textContent = message;
input.setAttribute('aria-describedby', `${input.getAttribute('aria-describedby') || ''} ${error.id}`.trim());
announce(message, 'assertive');
});
input.addEventListener('input', function () {
if (input.checkValidity()) {
input.removeAttribute('aria-invalid');
const error = qs(`#${CSS.escape(id)}-error`);
if (error) {
error.textContent = '';
}
}
});
});
});
}
function initTables() {
const tables = qsa(RXA11Y.selectors.table);
tables.forEach(function (table, index) {
if (!table.getAttribute('role')) {
table.setAttribute('role', 'table');
}
if (!table.getAttribute('aria-label') && !table.getAttribute('aria-labelledby')) {
const caption = qs('caption', table);
if (caption && hasText(caption)) {
table.setAttribute('aria-labelledby', ensureId(caption, 'rx-table-caption'));
} else {
table.setAttribute('aria-label', `Data table ${index + 1}`);
}
}
const headers = qsa('th', table);
headers.forEach(function (header) {
if (!header.getAttribute('scope')) {
const parentRow = header.parentElement;
const isFirstCell = parentRow && parentRow.firstElementChild === header;
header.setAttribute('scope', isFirstCell ? 'row' : 'col');
}
});
const wrapper = table.parentElement;
if (wrapper && !wrapper.classList.contains('rx-table-responsive')) {
if (table.scrollWidth > wrapper.clientWidth) {
wrapper.setAttribute('tabindex', '0');
wrapper.setAttribute('role', 'region');
wrapper.setAttribute('aria-label', 'Scrollable table');
}
}
});
}
function initImages() {
const images = qsa(RXA11Y.selectors.image);
images.forEach(function (image) {
if (image.hasAttribute('alt')) return;
const role = image.getAttribute('role');
const ariaHidden = image.getAttribute('aria-hidden');
if (role === 'presentation' || ariaHidden === 'true') {
image.setAttribute('alt', '');
return;
}
const nearbyCaption =
image.closest('figure') && qs('figcaption', image.closest('figure'));
if (nearbyCaption && hasText(nearbyCaption)) {
image.setAttribute('alt', nearbyCaption.textContent.trim());
} else {
image.setAttribute('alt', '');
}
});
}
function initIframes() {
const iframes = qsa(RXA11Y.selectors.iframe);
iframes.forEach(function (iframe, index) {
if (!iframe.getAttribute('title')) {
const src = iframe.getAttribute('src') || '';
if (src.includes('youtube') || src.includes('youtu.be')) {
iframe.setAttribute('title', 'Embedded YouTube video');
} else if (src.includes('maps')) {
iframe.setAttribute('title', 'Embedded map');
} else {
iframe.setAttribute('title', `Embedded content ${index + 1}`);
}
}
});
}
function initExternalLinks() {
const links = qsa(RXA11Y.selectors.externalLink);
links.forEach(function (link) {
const rel = link.getAttribute('rel') || '';
const relParts = rel.split(/\s+/).filter(Boolean);
['noopener', 'noreferrer'].forEach(function (value) {
if (!relParts.includes(value)) {
relParts.push(value);
}
});
link.setAttribute('rel', relParts.join(' '));
const hasWarning =
link.textContent.includes('opens in a new tab') ||
link.getAttribute('aria-label')?.includes('opens in a new tab');
if (!hasWarning) {
if (link.getAttribute('aria-label')) {
link.setAttribute(
'aria-label',
`${link.getAttribute('aria-label')} opens in a new tab`
);
} else {
link.appendChild(createScreenReaderText(' opens in a new tab'));
}
}
});
}
function initSmoothAnchorFocus() {
doc.addEventListener('click', function (event) {
const link = event.target.closest('a[href^="#"]');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href === '#') return;
const target = qs(href);
if (!target) return;
event.preventDefault();
if (!target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '-1');
}
target.scrollIntoView({
behavior: RXA11Y.state.reducedMotion ? 'auto' : 'smooth',
block: 'start'
});
safeFocus(target, {
preventScroll: true
});
if (history.pushState) {
history.pushState(null, '', href);
}
});
}
function initKeyboardShortcuts() {
doc.addEventListener('keydown', function (event) {
const tagName = event.target && event.target.tagName
? event.target.tagName.toLowerCase()
: '';
const isTyping =
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select' ||
event.target.isContentEditable;
if (isTyping) return;
if (event.key === '/') {
const searchInput = qs('input[type="search"], input[name="s"]');
if (searchInput) {
event.preventDefault();
safeFocus(searchInput, {
scroll: true
});
announce('Search field focused');
}
}
if (event.key === RXA11Y.keys.escape) {
qsa(`.${RXA11Y.classNames.submenuOpen}`).forEach(function (item) {
const trigger = qs('[aria-expanded="true"]', item);
item.classList.remove(RXA11Y.classNames.submenuOpen);
if (trigger) {
setExpanded(trigger, false);
}
});
}
});
}
function initAriaCurrentForLinks() {
const currentUrl = win.location.href.split('#')[0];
const links = qsa('a[href]');
links.forEach(function (link) {
const linkUrl = link.href.split('#')[0];
if (linkUrl === currentUrl && !link.getAttribute('aria-current')) {
link.setAttribute('aria-current', 'page');
}
});
}
function initBackToTopButton() {
const button = qs('[data-rx-back-to-top], .back-to-top');
if (!button) return;
if (!button.getAttribute('aria-label') && !hasText(button)) {
button.setAttribute('aria-label', 'Back to top');
}
button.addEventListener('click', function (event) {
event.preventDefault();
win.scrollTo({
top: 0,
behavior: RXA11Y.state.reducedMotion ? 'auto' : 'smooth'
});
safeFocus(body, {
preventScroll: true
});
announce('Back to top');
});
}
function initDisclosureWidgets() {
const triggers = qsa('[data-rx-disclosure]');
triggers.forEach(function (trigger) {
const targetSelector =
trigger.getAttribute('data-rx-disclosure') ||
trigger.getAttribute('aria-controls');
const target = targetSelector ? qs(targetSelector) : null;
if (!target) return;
const targetId = ensureId(target, 'rx-disclosure');
trigger.setAttribute('aria-controls', targetId);
if (!trigger.hasAttribute('aria-expanded')) {
setExpanded(trigger, false);
}
if (trigger.getAttribute('aria-expanded') !== 'true') {
setHidden(target, true);
}
trigger.addEventListener('click', function () {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
setExpanded(trigger, !expanded);
setHidden(target, expanded);
});
trigger.addEventListener('keydown', function (event) {
if (event.key === RXA11Y.keys.escape) {
setExpanded(trigger, false);
setHidden(target, true);
safeFocus(trigger);
}
});
});
}
function initDetailsSummary() {
const detailsElements = qsa('details');
detailsElements.forEach(function (details) {
const summary = qs('summary', details);
if (!summary) return;
if (!summary.getAttribute('role')) {
summary.setAttribute('role', 'button');
}
summary.setAttribute('aria-expanded', details.open ? 'true' : 'false');
details.addEventListener('toggle', function () {
summary.setAttribute('aria-expanded', details.open ? 'true' : 'false');
});
});
}
function initCommentReplyAccessibility() {
const replyLinks = qsa('.comment-reply-link');
replyLinks.forEach(function (link) {
if (!link.getAttribute('aria-label')) {
const comment = link.closest('.comment');
const author = comment ? qs('.comment-author, .fn', comment) : null;
if (author && hasText(author)) {
link.setAttribute('aria-label', `Reply to ${author.textContent.trim()}`);
} else {
link.setAttribute('aria-label', 'Reply to comment');
}
}
});
const cancelReply = qs('#cancel-comment-reply-link');
if (cancelReply && !cancelReply.getAttribute('aria-label')) {
cancelReply.setAttribute('aria-label', 'Cancel comment reply');
}
}
function initPaginationAccessibility() {
const paginations = qsa('.pagination, .nav-links, .page-numbers');
paginations.forEach(function (pagination, index) {
const nav = pagination.closest('nav') || pagination;
if (!nav.getAttribute('aria-label')) {
nav.setAttribute('aria-label', index === 0 ? 'Pagination' : `Pagination ${index + 1}`);
}
qsa('a, span', pagination).forEach(function (item) {
if (item.classList.contains('current')) {
item.setAttribute('aria-current', 'page');
}
});
});
}
function initBreadcrumbAccessibility() {
const breadcrumbs = qsa('.breadcrumb, .breadcrumbs, [data-rx-breadcrumb]');
breadcrumbs.forEach(function (breadcrumb) {
if (!breadcrumb.getAttribute('aria-label')) {
breadcrumb.setAttribute('aria-label', 'Breadcrumb');
}
const nav = breadcrumb.closest('nav');
if (nav && !nav.getAttribute('aria-label')) {
nav.setAttribute('aria-label', 'Breadcrumb');
}
const lastLink = qsa('a', breadcrumb).pop();
if (lastLink && !lastLink.getAttribute('aria-current')) {
lastLink.setAttribute('aria-current', 'page');
}
});
}
function initCardLinks() {
const cards = qsa('[data-rx-card-link], .post-card, .article-card');
cards.forEach(function (card) {
const mainLink = qs('a[href]', card);
if (!mainLink) return;
if (!card.getAttribute('tabindex')) {
card.setAttribute('tabindex', '0');
}
if (!card.getAttribute('role')) {
card.setAttribute('role', 'link');
}
if (!card.getAttribute('aria-label')) {
card.setAttribute('aria-label', mainLink.textContent.trim() || 'Read article');
}
card.addEventListener('keydown', function (event) {
if (event.key === RXA11Y.keys.enter || event.key === RXA11Y.keys.space || event.key === RXA11Y.keys.spaceLegacy) {
event.preventDefault();
mainLink.click();
}
});
});
}
function initVideoControlsAccessibility() {
const videos = qsa('video');
videos.forEach(function (video, index) {
if (!video.getAttribute('aria-label') && !video.getAttribute('aria-labelledby')) {
video.setAttribute('aria-label', `Video content ${index + 1}`);
}
if (video.autoplay && !video.muted) {
video.muted = true;
}
if (RXA11Y.state.reducedMotion && video.autoplay) {
video.pause();
video.removeAttribute('autoplay');
}
});
}
function initLazyLoadedContentObserver() {
if (!('MutationObserver' in win)) return;
const observer = new MutationObserver(function (mutations) {
let shouldRefresh = false;
mutations.forEach(function (mutation) {
if (mutation.addedNodes && mutation.addedNodes.length) {
shouldRefresh = true;
}
});
if (!shouldRefresh) return;
win.clearTimeout(initLazyLoadedContentObserver.timer);
initLazyLoadedContentObserver.timer = win.setTimeout(function () {
initImages();
initIframes();
initExternalLinks();
initFormAccessibility();
initTables();
initCommentReplyAccessibility();
initPaginationAccessibility();
}, 250);
});
observer.observe(body, {
childList: true,
subtree: true
});
}
function initPrintAccessibility() {
win.addEventListener('beforeprint', function () {
qsa('[hidden][data-rx-print-show]').forEach(function (element) {
element.dataset.rxWasHidden = 'true';
element.removeAttribute('hidden');
});
});
win.addEventListener('afterprint', function () {
qsa('[data-rx-was-hidden="true"]').forEach(function (element) {
element.setAttribute('hidden', '');
delete element.dataset.rxWasHidden;
});
});
}
function initHashFocusOnLoad() {
if (!win.location.hash) return;
win.setTimeout(function () {
const target = qs(win.location.hash);
if (!target) return;
if (!target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '-1');
}
safeFocus(target, {
scroll: true
});
}, 300);
}
function initWpAdminBarOffset() {
const adminBar = qs('#wpadminbar');
if (!adminBar) return;
const height = adminBar.offsetHeight || 32;
html.style.setProperty('--rx-admin-bar-height', `${height}px`);
}
function initAccessibleNamesForButtons() {
const buttons = qsa('button, [role="button"]');
buttons.forEach(function (button, index) {
const hasName =
hasText(button) ||
button.getAttribute('aria-label') ||
button.getAttribute('aria-labelledby') ||
button.getAttribute('title');
if (!hasName) {
button.setAttribute('aria-label', `Button ${index + 1}`);
}
});
}
function initNoHrefLinks() {
const links = qsa('a:not([href])');
links.forEach(function (link) {
if (!link.getAttribute('role')) {
link.setAttribute('role', 'button');
}
if (!link.getAttribute('tabindex')) {
link.setAttribute('tabindex', '0');
}
link.addEventListener('keydown', function (event) {
if (event.key === RXA11Y.keys.enter || event.key === RXA11Y.keys.space || event.key === RXA11Y.keys.spaceLegacy) {
event.preventDefault();
link.click();
}
});
});
}
function initRequiredFieldLabels() {
const requiredFields = qsa('input[required], textarea[required], select[required]');
requiredFields.forEach(function (field) {
field.setAttribute('aria-required', 'true');
const id = ensureId(field, 'rx-required-field');
const label = qs(`label[for="${CSS.escape(id)}"]`);
if (label && !label.querySelector('.rx-required-text')) {
const requiredText = createScreenReaderText(' required');
requiredText.classList.add('rx-required-text');
label.appendChild(requiredText);
}
});
}
function initContentEditableAccessibility() {
const editables = qsa('[contenteditable="true"]');
editables.forEach(function (editable, index) {
if (!editable.getAttribute('role')) {
editable.setAttribute('role', 'textbox');
}
if (!editable.getAttribute('aria-label') && !editable.getAttribute('aria-labelledby')) {
editable.setAttribute('aria-label', `Editable text area ${index + 1}`);
}
if (!editable.getAttribute('tabindex')) {
editable.setAttribute('tabindex', '0');
}
});
}
function initProgressBars() {
const progressBars = qsa('[data-rx-progress], .progress-bar');
progressBars.forEach(function (bar) {
if (!bar.getAttribute('role')) {
bar.setAttribute('role', 'progressbar');
}
const value =
bar.getAttribute('data-value') ||
bar.style.width.replace('%', '') ||
bar.getAttribute('aria-valuenow') ||
'0';
bar.setAttribute('aria-valuemin', bar.getAttribute('aria-valuemin') || '0');
bar.setAttribute('aria-valuemax', bar.getAttribute('aria-valuemax') || '100');
bar.setAttribute('aria-valuenow', value);
});
}
function initStatusMessages() {
const statuses = qsa('[data-rx-status], .rx-status, .notice, .alert');
statuses.forEach(function (status) {
if (!status.getAttribute('role')) {
const isError =
status.classList.contains('error') ||
status.classList.contains('alert') ||
status.classList.contains('notice-error');
status.setAttribute('role', isError ? 'alert' : 'status');
}
if (!status.getAttribute('aria-live')) {
status.setAttribute(
'aria-live',
status.getAttribute('role') === 'alert' ? 'assertive' : 'polite'
);
}
});
}
function initReadableTimeElements() {
const times = qsa('time');
times.forEach(function (time) {
if (!time.getAttribute('datetime')) {
const text = time.textContent.trim();
if (text) {
time.setAttribute('datetime', text);
}
}
});
}
function initCodeBlocks() {
const codeBlocks = qsa('pre');
codeBlocks.forEach(function (pre, index) {
if (!pre.getAttribute('tabindex')) {
pre.setAttribute('tabindex', '0');
}
if (!pre.getAttribute('role')) {
pre.setAttribute('role', 'region');
}
if (!pre.getAttribute('aria-label')) {
pre.setAttribute('aria-label', `Code block ${index + 1}`);
}
});
}
function initRxA11YApi() {
win.RXThemeA11Y = {
version: RXA11Y.version,
announce,
focus: safeFocus,
getFocusableElements,
refresh: function () {
initLandmarks();
initImages();
initIframes();
initExternalLinks();
initFormAccessibility();
initTables();
initAccessibleNamesForButtons();
initRequiredFieldLabels();
initStatusMessages();
},
openModal: function (selector) {
const modal = qs(selector);
if (!modal) return;
RXA11Y.state.lastFocusedElement = doc.activeElement;
RXA11Y.state.activeModal = modal;
setHidden(modal, false);
body.classList.add(RXA11Y.classNames.modalOpen);
const focusTarget = getFocusableElements(modal)[0] || modal;
if (!modal.hasAttribute('tabindex')) {
modal.setAttribute('tabindex', '-1');
}
safeFocus(focusTarget);
},
closeModal: function () {
const modal = RXA11Y.state.activeModal;
if (!modal) return;
setHidden(modal, true);
body.classList.remove(RXA11Y.classNames.modalOpen);
if (RXA11Y.state.lastFocusedElement) {
safeFocus(RXA11Y.state.lastFocusedElement);
}
RXA11Y.state.activeModal = null;
}
};
}
function init() {
if (RXA11Y.state.initialized) return;
RXA11Y.state.initialized = true;
initBaseClasses();
initReducedMotion();
initInputModeDetection();
initFocusWithinPolyfill();
initSkipLinks();
initLandmarks();
initMobileMenu();
initDropdownMenus();
initModals();
initAccordions();
initTabs();
initSearchForms();
initFormAccessibility();
initTables();
initImages();
initIframes();
initExternalLinks();
initSmoothAnchorFocus();
initKeyboardShortcuts();
initAriaCurrentForLinks();
initBackToTopButton();
initDisclosureWidgets();
initDetailsSummary();
initCommentReplyAccessibility();
initPaginationAccessibility();
initBreadcrumbAccessibility();
initCardLinks();
initVideoControlsAccessibility();
initLazyLoadedContentObserver();
initPrintAccessibility();
initHashFocusOnLoad();
initWpAdminBarOffset();
initAccessibleNamesForButtons();
initNoHrefLinks();
initRequiredFieldLabels();
initContentEditableAccessibility();
initProgressBars();
initStatusMessages();
initReadableTimeElements();
initCodeBlocks();
initRxA11YApi();
announce('Accessibility features loaded');
}
if (doc.readyState === 'loading') {
doc.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Add this enqueue in your RX Theme functions.php or your enqueue file:
function rx_theme_enqueue_accessibility_chunk() {
wp_enqueue_script(
'rx-theme-accessibility-chunk',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-021-accessibility.js',
array(),
'1.0.0',
true
);
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_accessibility_chunk' );
For best result, also add this small CSS:
.screen-reader-text,
.rx-visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.screen-reader-text:focus,
.skip-link:focus {
position: fixed !important;
top: calc(12px + var(--rx-admin-bar-height, 0px)) !important;
left: 12px !important;
z-index: 999999 !important;
width: auto !important;
height: auto !important;
padding: 12px 18px !important;
margin: 0 !important;
overflow: visible !important;
clip: auto !important;
white-space: normal !important;
background: #ffffff !important;
color: #111111 !important;
border: 2px solid currentColor !important;
border-radius: 6px !important;
text-decoration: none !important;
}
.rx-keyboard-user :focus {
outline: 3px solid currentColor;
outline-offset: 3px;
}
.rx-mouse-user :focus {
outline: none;
}
.rx-reduced-motion *,
.rx-reduced-motion *::before,
.rx-reduced-motion *::after {
scroll-behavior: auto !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.rx-modal-open {
overflow: hidden;
}
[hidden] {
display: none !important;
}
.rx-table-responsive {
overflow-x: auto;
}
Best filename is exactly:
chunk-021-accessibility.js
This is a good advanced accessibility chunk for RX Theme. It is large but still safe, clean, and performance-friendly.