All files / lib/core/runtime/navigation A11yManager.js

94.05% Statements 95/101
86.36% Branches 19/22
100% Functions 3/3
94.05% Lines 95/101

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 10147x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 15x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 15x 47x 47x 47x 47x 47x 47x 47x 13x 13x 13x 13x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x             13x 47x 47x 47x 47x 47x 47x 47x 47x 62x 62x 62x 62x 62x 62x 62x 62x 62x 62x 60x 60x 62x 62x 19x 6x 6x 6x 6x 19x 19x 62x
/**
 * Accessibility helper for managing focus and route announcements during SPA navigation.
 */
 
const LIVE_REGION_ID = '__avenx_a11y_announcer__';
 
/**
 * Creates or retrieves the singleton visually hidden aria-live region.
 * @returns {HTMLElement|null}
 */
export function getOrCreateLiveRegion() {
  if (typeof document === 'undefined' || typeof document.getElementById !== 'function' || typeof document.createElement !== 'function' || !document.body || typeof document.body.appendChild !== 'function') return null;
 
  let region = document.getElementById(LIVE_REGION_ID);
  if (!region) {
    region = document.createElement('div');
    region.id = LIVE_REGION_ID;
    if (typeof region.setAttribute === 'function') {
      region.setAttribute('role', 'status');
      region.setAttribute('aria-live', 'polite');
      region.setAttribute('aria-atomic', 'true');
    }
 
    // Standard accessible visually-hidden CSS (clip-rect pattern)
    if (region.style && typeof region.style === 'object') {
      Object.assign(region.style, {
        position: 'absolute',
        width: '1px',
        height: '1px',
        padding: '0',
        margin: '-1px',
        overflow: 'hidden',
        clip: 'rect(0, 0, 0, 0)',
        whiteSpace: 'nowrap',
        border: '0',
      });
    }
 
    document.body.appendChild(region);
  }
  return region;
}
 
/**
 * Announces a message to screen readers via the live region.
 * Clears and updates after a microtask/frame to ensure assistive tech detects changes.
 * @param {string} message
 */
export function announce(message) {
  if (typeof document === 'undefined' || !message) return;
 
  const region = getOrCreateLiveRegion();
  if (!region) return;
 
  // Clear first so identical consecutive titles are re-announced
  region.textContent = '';
 
  if (typeof requestAnimationFrame !== 'undefined') {
    requestAnimationFrame(() => {
      if (region) {
        region.textContent = message;
      }
    });
  } else {
    setTimeout(() => {
      if (region) {
        region.textContent = message;
      }
    }, 0);
  }
}
 
/**
 * Moves focus to the new page container or configured target.
 * Applies tabindex="-1" if needed so non-focusable elements can receive focus.
 * @param {HTMLElement|null} container - The mounted page root element.
 * @param {string} [focusTargetSelector] - Optional custom selector.
 */
export function manageFocus(container, focusTargetSelector) {
  if (typeof document === 'undefined' || !container) return;
 
  let target = null;
 
  if (focusTargetSelector && typeof focusTargetSelector === 'string') {
    target = typeof container.querySelector === 'function' ? container.querySelector(focusTargetSelector) : null;
  }
 
  // Fallback to data-ax-page-heading if present, else page container
  if (!target) {
    target = (typeof container.querySelector === 'function' && container.querySelector('[data-ax-page-heading]')) || container;
  }
 
  if (target && typeof target.focus === 'function') {
    if (typeof target.hasAttribute === 'function' && !target.hasAttribute('tabindex')) {
      if (typeof target.setAttribute === 'function') {
        target.setAttribute('tabindex', '-1');
      }
    }
    target.focus({ preventScroll: true });
  }
}