All files / src/components/ui/LiveRegion index.tsx

0% Statements 0/123
100% Branches 0/0
0% Functions 0/1
0% Lines 0/123

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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                                                                                                                                                                                                                                       
'use client';

import { createContext, useContext, useState, useCallback, ReactNode } from 'react';

/**
 * LiveRegion Context Type
 * Provides methods for announcing messages to screen readers
 */
interface LiveRegionContextType {
  /**
   * Announce a message to screen readers
   * @param message - The message to announce
   * @param priority - 'polite' (default) waits for user to finish, 'assertive' interrupts immediately
   */
  announce: (message: string, priority?: 'polite' | 'assertive') => void;
}

const LiveRegionContext = createContext<LiveRegionContextType | null>(null);

interface LiveRegionProviderProps {
  children: ReactNode;
}

/**
 * LiveRegionProvider Component
 *
 * Provides a way to announce dynamic content changes to screen readers.
 * Wraps the application and provides the useAnnounce hook.
 *
 * @example
 * ```tsx
 * // In your layout or app root:
 * <LiveRegionProvider>
 *   <App />
 * </LiveRegionProvider>
 *
 * // In a component:
 * function AddToCartButton({ product }) {
 *   const announce = useAnnounce();
 *
 *   const handleAddToCart = () => {
 *     addToCart(product);
 *     announce(`${product.name} added to cart`);
 *   };
 *
 *   return <button onClick={handleAddToCart}>Add to Cart</button>;
 * }
 * ```
 */
export function LiveRegionProvider({ children }: LiveRegionProviderProps) {
  const [politeMessage, setPoliteMessage] = useState('');
  const [assertiveMessage, setAssertiveMessage] = useState('');

  const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
    if (priority === 'assertive') {
      // Clear first, then set after a brief delay to ensure screen readers pick up the change
      setAssertiveMessage('');
      setTimeout(() => setAssertiveMessage(message), 100);
      // Auto-clear after announcement
      setTimeout(() => setAssertiveMessage(''), 1000);
    } else {
      setPoliteMessage('');
      setTimeout(() => setPoliteMessage(message), 100);
      setTimeout(() => setPoliteMessage(''), 1000);
    }
  }, []);

  return (
    <LiveRegionContext.Provider value={{ announce }}>
      {children}

      {/* Polite announcements (non-interrupting) - used for status updates */}
      <div
        role="status"
        aria-live="polite"
        aria-atomic="true"
        className="sr-only"
      >
        {politeMessage}
      </div>

      {/* Assertive announcements (interrupting) - used for urgent messages */}
      <div
        role="alert"
        aria-live="assertive"
        aria-atomic="true"
        className="sr-only"
      >
        {assertiveMessage}
      </div>
    </LiveRegionContext.Provider>
  );
}

/**
 * useAnnounce Hook
 *
 * Returns a function to announce messages to screen readers.
 * Must be used within a LiveRegionProvider.
 *
 * @returns announce function
 * @throws Error if used outside of LiveRegionProvider
 *
 * @example
 * ```tsx
 * const announce = useAnnounce();
 *
 * // Polite announcement (default) - doesn't interrupt user
 * announce('Item added to cart');
 *
 * // Assertive announcement - interrupts immediately for urgent messages
 * announce('Error: Payment failed', 'assertive');
 * ```
 */
export function useAnnounce() {
  const context = useContext(LiveRegionContext);
  if (!context) {
    throw new Error('useAnnounce must be used within a LiveRegionProvider');
  }
  return context.announce;
}

export default LiveRegionProvider;