CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-aria--utils

Essential utility functions and React hooks for building accessible React Aria UI components

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

shadow-dom-support.mddocs/

Shadow DOM Support

Complete Shadow DOM traversal, manipulation, and compatibility utilities for working with web components and shadow roots.

Capabilities

Shadow Tree Walker

Class implementing TreeWalker interface with Shadow DOM traversal support.

/**
 * TreeWalker implementation with Shadow DOM support
 * Implements the full TreeWalker interface for traversing DOM trees
 * that may contain Shadow DOM boundaries
 */
class ShadowTreeWalker implements TreeWalker {
  readonly root: Node;
  readonly whatToShow: number;
  readonly filter: NodeFilter | null;
  currentNode: Node;
  
  parentNode(): Node | null;
  firstChild(): Node | null;
  lastChild(): Node | null;
  previousSibling(): Node | null;
  nextSibling(): Node | null;
  previousNode(): Node | null;
  nextNode(): Node | null;
}

/**
 * Creates Shadow DOM-aware TreeWalker
 * @param doc - Document context
 * @param root - Root node to traverse from
 * @param whatToShow - Node types to show (NodeFilter constants)
 * @param filter - Optional node filter
 * @returns TreeWalker implementation with shadow DOM support
 */
function createShadowTreeWalker(
  doc: Document,
  root: Node,
  whatToShow?: number,
  filter?: NodeFilter | null
): TreeWalker;

Usage Examples:

import { ShadowTreeWalker, createShadowTreeWalker } from "@react-aria/utils";

// Traverse entire DOM tree including shadow DOM
function traverseAllNodes(rootElement: Element) {
  const walker = createShadowTreeWalker(
    document,
    rootElement,
    NodeFilter.SHOW_ELEMENT
  );
  
  const allElements: Element[] = [];
  let currentNode = walker.currentNode as Element;
  
  // Collect all elements including those in shadow DOM
  do {
    allElements.push(currentNode);
    currentNode = walker.nextNode() as Element;
  } while (currentNode);
  
  return allElements;
}

// Find focusable elements across shadow boundaries
function findAllFocusableElements(container: Element): Element[] {
  const walker = createShadowTreeWalker(
    document,
    container,
    NodeFilter.SHOW_ELEMENT,
    {
      acceptNode: (node) => {
        const element = node as Element;
        
        // Check if element is focusable
        if (element.matches('button, input, select, textarea, a[href], [tabindex]')) {
          return NodeFilter.FILTER_ACCEPT;
        }
        
        return NodeFilter.FILTER_SKIP;
      }
    }
  );
  
  const focusableElements: Element[] = [];
  let currentNode = walker.nextNode() as Element;
  
  while (currentNode) {
    focusableElements.push(currentNode);
    currentNode = walker.nextNode() as Element;
  }
  
  return focusableElements;
}

// Custom TreeWalker with shadow DOM support
function createCustomWalker(root: Element, filterFunction: (node: Element) => boolean) {
  return createShadowTreeWalker(
    document,
    root,
    NodeFilter.SHOW_ELEMENT,
    {
      acceptNode: (node) => {
        return filterFunction(node as Element) 
          ? NodeFilter.FILTER_ACCEPT 
          : NodeFilter.FILTER_SKIP;
      }
    }
  );
}

Shadow DOM Utilities

Functions for safely working with Shadow DOM elements and events.

/**
 * Shadow DOM-safe version of document.activeElement
 * @param doc - Document to get active element from (default: document)
 * @returns Currently focused element, traversing into shadow roots
 */
function getActiveElement(doc?: Document): Element | null;

/**
 * Shadow DOM-safe version of event.target
 * @param event - Event to get target from
 * @returns Event target, using composedPath() when available
 */
function getEventTarget<T extends Event>(event: T): Element | null;

/**
 * Shadow DOM-safe version of Node.contains()
 * @param node - Container node
 * @param otherNode - Node to check containment for
 * @returns Boolean indicating containment across shadow boundaries
 */
function nodeContains(node: Node, otherNode: Node): boolean;

Usage Examples:

import { getActiveElement, getEventTarget, nodeContains } from "@react-aria/utils";

// Focus management across shadow boundaries
function FocusManager({ children }) {
  const containerRef = useRef<HTMLDivElement>(null);
  
  const handleFocusOut = (e: FocusEvent) => {
    // Get the actual focused element (may be in shadow DOM)
    const activeElement = getActiveElement();
    const container = containerRef.current;
    
    if (container && activeElement) {
      // Check if focus moved outside container (across shadow boundaries)
      if (!nodeContains(container, activeElement)) {
        console.log('Focus moved outside container');
        onFocusLeave();
      }
    }
  };
  
  const handleGlobalClick = (e: MouseEvent) => {
    // Get actual click target (may be in shadow DOM)
    const target = getEventTarget(e);
    const container = containerRef.current;
    
    if (container && target) {
      // Check if click was outside container
      if (!nodeContains(container, target)) {
        console.log('Clicked outside container');
        onClickOutside();
      }
    }
  };
  
  useEffect(() => {
    document.addEventListener('click', handleGlobalClick);
    return () => document.removeEventListener('click', handleGlobalClick);
  }, []);
  
  return (
    <div ref={containerRef} onFocusOut={handleFocusOut}>
      {children}
    </div>
  );
}

// Modal with shadow DOM support
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!isOpen) return;
    
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        onClose();
      }
    };
    
    const handleClickOutside = (e: MouseEvent) => {
      const target = getEventTarget(e);
      const modal = modalRef.current;
      
      if (modal && target && !nodeContains(modal, target)) {
        onClose();
      }
    };
    
    document.addEventListener('keydown', handleEscape);
    document.addEventListener('mousedown', handleClickOutside);
    
    return () => {
      document.removeEventListener('keydown', handleEscape);
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [isOpen, onClose]);
  
  // Focus first element when modal opens
  useEffect(() => {
    if (isOpen && modalRef.current) {
      const focusableElements = findAllFocusableElements(modalRef.current);
      if (focusableElements.length > 0) {
        (focusableElements[0] as HTMLElement).focus();
      }
    }
  }, [isOpen]);
  
  return isOpen ? (
    <div className="modal-backdrop">
      <div ref={modalRef} className="modal-content" role="dialog" aria-modal="true">
        {children}
      </div>
    </div>
  ) : null;
}

// Dropdown menu with shadow DOM event handling
function DropdownMenu({ trigger, children }) {
  const [isOpen, setIsOpen] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!isOpen) return;
    
    const handleGlobalClick = (e: MouseEvent) => {
      const target = getEventTarget(e);
      const container = containerRef.current;
      
      if (container && target && !nodeContains(container, target)) {
        setIsOpen(false);
      }
    };
    
    // Use capture phase to ensure we get the event before shadow DOM
    document.addEventListener('mousedown', handleGlobalClick, true);
    
    return () => {
      document.removeEventListener('mousedown', handleGlobalClick, true);
    };
  }, [isOpen]);
  
  return (
    <div ref={containerRef} className="dropdown">
      <div onClick={() => setIsOpen(!isOpen)}>
        {trigger}
      </div>
      {isOpen && (
        <div className="dropdown-menu">
          {children}
        </div>
      )}
    </div>
  );
}

Advanced Shadow DOM Patterns

Complex scenarios involving web components and shadow DOM boundaries:

import { 
  createShadowTreeWalker, 
  getActiveElement, 
  getEventTarget, 
  nodeContains 
} from "@react-aria/utils";

// Web component integration
function WebComponentWrapper({ children }) {
  const wrapperRef = useRef<HTMLDivElement>(null);
  
  const findElementsInShadowDOM = useCallback((selector: string) => {
    if (!wrapperRef.current) return [];
    
    const walker = createShadowTreeWalker(
      document,
      wrapperRef.current,
      NodeFilter.SHOW_ELEMENT,
      {
        acceptNode: (node) => {
          const element = node as Element;
          return element.matches(selector) 
            ? NodeFilter.FILTER_ACCEPT 
            : NodeFilter.FILTER_SKIP;
        }
      }
    );
    
    const elements: Element[] = [];
    let currentNode = walker.nextNode() as Element;
    
    while (currentNode) {
      elements.push(currentNode);
      currentNode = walker.nextNode() as Element;
    }
    
    return elements;
  }, []);
  
  const handleInteraction = (e: Event) => {
    // Get the actual target even if it's in shadow DOM
    const target = getEventTarget(e);
    
    if (target) {
      console.log('Interaction with element:', target.tagName);
      
      // Find all related elements in shadow DOM
      const relatedElements = findElementsInShadowDOM('[data-related]');
      relatedElements.forEach(el => {
        el.classList.add('highlighted');
      });
    }
  };
  
  return (
    <div 
      ref={wrapperRef}
      onMouseOver={handleInteraction}
      onFocus={handleInteraction}
    >
      {children}
    </div>
  );
}

// Cross-shadow-boundary focus trap
function ShadowAwareFocusTrap({ isActive, children }) {
  const containerRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!isActive || !containerRef.current) return;
    
    // Find all focusable elements including those in shadow DOM
    const focusableElements = findAllFocusableElements(containerRef.current);
    
    if (focusableElements.length === 0) return;
    
    const firstFocusable = focusableElements[0] as HTMLElement;
    const lastFocusable = focusableElements[focusableElements.length - 1] as HTMLElement;
    
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return;
      
      const activeElement = getActiveElement();
      
      if (e.shiftKey) {
        // Shift+Tab: wrap to last if on first
        if (activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable.focus();
        }
      } else {
        // Tab: wrap to first if on last  
        if (activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable.focus();
        }
      }
    };
    
    // Focus first element initially
    firstFocusable.focus();
    
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isActive]);
  
  return (
    <div ref={containerRef}>
      {children}
    </div>
  );
}

// Event delegation across shadow boundaries
function ShadowAwareEventDelegation({ onButtonClick, children }) {
  const containerRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    
    const handleClick = (e: MouseEvent) => {
      const target = getEventTarget(e);
      
      if (target && target.matches('button, [role="button"]')) {
        // Check if the button is contained within our container
        if (nodeContains(container, target)) {
          onButtonClick(target, e);
        }
      }
    };
    
    // Use capture to ensure we get events from shadow DOM
    document.addEventListener('click', handleClick, true);
    
    return () => {
      document.removeEventListener('click', handleClick, true);
    };
  }, [onButtonClick]);
  
  return (
    <div ref={containerRef}>
      {children}
    </div>
  );
}

Performance Considerations

Shadow DOM utilities are designed for performance:

  • TreeWalker: Uses native browser TreeWalker when possible, falls back to custom implementation
  • Event targeting: Leverages composedPath() when available for efficient shadow DOM traversal
  • Containment checking: Optimized algorithms for cross-boundary containment checks
  • Caching: Active element and event target results are not cached to ensure accuracy

Browser Compatibility

These utilities provide consistent behavior across all modern browsers:

  • Chrome/Edge: Full native Shadow DOM support
  • Firefox: Full native Shadow DOM support
  • Safari: Full native Shadow DOM support
  • Older browsers: Graceful fallback to standard DOM methods

Types

interface TreeWalker {
  readonly root: Node;
  readonly whatToShow: number;
  readonly filter: NodeFilter | null;
  currentNode: Node;
  
  parentNode(): Node | null;
  firstChild(): Node | null;
  lastChild(): Node | null;
  previousSibling(): Node | null;
  nextSibling(): Node | null;
  previousNode(): Node | null;
  nextNode(): Node | null;
}

interface NodeFilter {
  acceptNode(node: Node): number;
}

declare const NodeFilter: {
  readonly FILTER_ACCEPT: 1;
  readonly FILTER_REJECT: 2;
  readonly FILTER_SKIP: 3;
  readonly SHOW_ALL: 0xFFFFFFFF;
  readonly SHOW_ELEMENT: 0x1;
  readonly SHOW_ATTRIBUTE: 0x2;
  readonly SHOW_TEXT: 0x4;
  readonly SHOW_CDATA_SECTION: 0x8;
  readonly SHOW_ENTITY_REFERENCE: 0x10;
  readonly SHOW_ENTITY: 0x20;
  readonly SHOW_PROCESSING_INSTRUCTION: 0x40;
  readonly SHOW_COMMENT: 0x80;
  readonly SHOW_DOCUMENT: 0x100;
  readonly SHOW_DOCUMENT_TYPE: 0x200;
  readonly SHOW_DOCUMENT_FRAGMENT: 0x400;
  readonly SHOW_NOTATION: 0x800;
};

Install with Tessl CLI

npx tessl i tessl/npm-react-aria--utils

docs

animation-and-transitions.md

event-management.md

focus-and-accessibility.md

id-and-refs.md

index.md

links-and-navigation.md

miscellaneous-utilities.md

platform-detection.md

props-and-events.md

scrolling-and-layout.md

shadow-dom-support.md

state-and-effects.md

virtual-events-and-input.md

tile.json