CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-aria--focus

React hooks and components for accessible focus management including FocusScope for focus containment, FocusRing for visual focus indicators, and utilities for focus navigation and virtual focus handling.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

virtual-focus.mddocs/

Virtual Focus System

Support for aria-activedescendant focus patterns commonly used in comboboxes, listboxes, and other composite widgets where focus remains on a container while a descendant is highlighted.

Capabilities

moveVirtualFocus Function

Moves virtual focus from the current element to a target element, properly dispatching blur and focus events.

/**
 * Moves virtual focus from current element to target element.
 * Dispatches appropriate virtual blur and focus events.
 */
function moveVirtualFocus(to: Element | null): void;

Usage Examples:

import React, { useRef, useState } from "react";
import { moveVirtualFocus } from "@react-aria/focus";

// Listbox with virtual focus
function VirtualListbox({ options, value, onChange }) {
  const listboxRef = useRef<HTMLDivElement>(null);
  const [activeIndex, setActiveIndex] = useState(0);
  const optionRefs = useRef<(HTMLDivElement | null)[]>([]);
  
  const moveToOption = (index: number) => {
    if (index >= 0 && index < options.length) {
      const option = optionRefs.current[index];
      if (option) {
        moveVirtualFocus(option);
        setActiveIndex(index);
        
        // Update aria-activedescendant on the listbox
        if (listboxRef.current) {
          listboxRef.current.setAttribute('aria-activedescendant', option.id);
        }
      }
    }
  };
  
  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        moveToOption(Math.min(activeIndex + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        moveToOption(Math.max(activeIndex - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        onChange(options[activeIndex]);
        break;
    }
  };
  
  return (
    <div
      ref={listboxRef}
      role="listbox"
      tabIndex={0}
      onKeyDown={handleKeyDown}
      aria-activedescendant={`option-${activeIndex}`}
    >
      {options.map((option, index) => (
        <div
          key={index}
          ref={(el) => (optionRefs.current[index] = el)}
          id={`option-${index}`}
          role="option"
          aria-selected={value === option}
          onClick={() => {
            moveToOption(index);
            onChange(option);
          }}
        >
          {option}
        </div>
      ))}
    </div>
  );
}

// Combobox with virtual focus
function VirtualCombobox({ options, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const inputRef = useRef<HTMLInputElement>(null);
  const optionRefs = useRef<(HTMLDivElement | null)[]>([]);
  
  const moveToOption = (index: number) => {
    if (index >= 0 && index < options.length) {
      const option = optionRefs.current[index];
      if (option) {
        moveVirtualFocus(option);
        setActiveIndex(index);
        
        // Update aria-activedescendant on the input
        if (inputRef.current) {
          inputRef.current.setAttribute('aria-activedescendant', option.id);
        }
      }
    } else {
      // Clear virtual focus
      moveVirtualFocus(null);
      setActiveIndex(-1);
      if (inputRef.current) {
        inputRef.current.removeAttribute('aria-activedescendant');
      }
    }
  };
  
  return (
    <div className="combobox">
      <input
        ref={inputRef}
        type="text"
        role="combobox"
        aria-expanded={isOpen}
        aria-haspopup="listbox"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onFocus={() => setIsOpen(true)}
        onKeyDown={(e) => {
          if (!isOpen) return;
          
          switch (e.key) {
            case 'ArrowDown':
              e.preventDefault();
              moveToOption(activeIndex + 1);
              break;
            case 'ArrowUp':
              e.preventDefault();
              moveToOption(activeIndex - 1);
              break;
            case 'Enter':
              if (activeIndex >= 0) {
                e.preventDefault();
                onChange(options[activeIndex]);
                setIsOpen(false);
              }
              break;
            case 'Escape':
              setIsOpen(false);
              moveToOption(-1);
              break;
          }
        }}
      />
      {isOpen && (
        <div role="listbox">
          {options.map((option, index) => (
            <div
              key={index}
              ref={(el) => (optionRefs.current[index] = el)}
              id={`combobox-option-${index}`}
              role="option"
              onClick={() => {
                onChange(option);
                setIsOpen(false);
              }}
            >
              {option}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

dispatchVirtualBlur Function

Dispatches virtual blur events on an element when virtual focus is moving away from it.

/**
 * Dispatches virtual blur events on element when virtual focus moves away.
 */
function dispatchVirtualBlur(from: Element, to: Element | null): void;

dispatchVirtualFocus Function

Dispatches virtual focus events on an element when virtual focus is moving to it.

/**
 * Dispatches virtual focus events on element when virtual focus moves to it.
 */
function dispatchVirtualFocus(to: Element, from: Element | null): void;

Usage Examples:

import React, { useRef } from "react";
import { dispatchVirtualBlur, dispatchVirtualFocus } from "@react-aria/focus";

// Custom virtual focus implementation
function CustomVirtualFocus({ items }) {
  const [focusedIndex, setFocusedIndex] = useState(-1);
  const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
  const previousFocusedRef = useRef<Element | null>(null);
  
  const setVirtualFocus = (index: number) => {
    const previousElement = previousFocusedRef.current;
    const newElement = index >= 0 ? itemRefs.current[index] : null;
    
    // Dispatch blur event on previously focused element
    if (previousElement && previousElement !== newElement) {
      dispatchVirtualBlur(previousElement, newElement);
    }
    
    // Dispatch focus event on newly focused element
    if (newElement && newElement !== previousElement) {
      dispatchVirtualFocus(newElement, previousElement);
    }
    
    previousFocusedRef.current = newElement;
    setFocusedIndex(index);
  };
  
  return (
    <div>
      {items.map((item, index) => (
        <div
          key={index}
          ref={(el) => (itemRefs.current[index] = el)}
          onVirtualFocus={() => console.log(`Virtual focus on ${item}`)}
          onVirtualBlur={() => console.log(`Virtual blur from ${item}`)}
          onClick={() => setVirtualFocus(index)}
          style={{
            backgroundColor: focusedIndex === index ? '#e0e0e0' : 'transparent'
          }}
        >
          {item}
        </div>
      ))}
      <button onClick={() => setVirtualFocus(-1)}>Clear Virtual Focus</button>
    </div>
  );
}

// Event listener example
function VirtualFocusEventListener() {
  const itemRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    const element = itemRef.current;
    if (!element) return;
    
    const handleVirtualFocus = (e: Event) => {
      console.log('Virtual focus received:', e);
      element.classList.add('virtually-focused');
    };
    
    const handleVirtualBlur = (e: Event) => {
      console.log('Virtual blur received:', e);
      element.classList.remove('virtually-focused');
    };
    
    element.addEventListener('focus', handleVirtualFocus);
    element.addEventListener('blur', handleVirtualBlur);
    
    return () => {
      element.removeEventListener('focus', handleVirtualFocus);
      element.removeEventListener('blur', handleVirtualBlur);
    };
  }, []);
  
  return (
    <div>
      <div ref={itemRef}>Virtual focusable item</div>
      <button 
        onClick={() => dispatchVirtualFocus(itemRef.current!, null)}
      >
        Focus Item
      </button>
      <button 
        onClick={() => dispatchVirtualBlur(itemRef.current!, null)}
      >
        Blur Item
      </button>
    </div>
  );
}

getVirtuallyFocusedElement Function

Gets the currently virtually focused element using aria-activedescendant or the active element.

/**
 * Gets currently virtually focused element using aria-activedescendant
 * or falls back to the active element.
 */
function getVirtuallyFocusedElement(document: Document): Element | null;

Usage Examples:

import React, { useEffect, useState } from "react";
import { getVirtuallyFocusedElement } from "@react-aria/focus";

// Virtual focus tracker
function VirtualFocusTracker() {
  const [virtuallyFocused, setVirtuallyFocused] = useState<Element | null>(null);
  
  useEffect(() => {
    const updateVirtualFocus = () => {
      const element = getVirtuallyFocusedElement(document);
      setVirtuallyFocused(element);
    };
    
    // Update on focus changes
    document.addEventListener('focusin', updateVirtualFocus);
    document.addEventListener('focusout', updateVirtualFocus);
    
    // Update when aria-activedescendant changes
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type === 'attributes' && 
            mutation.attributeName === 'aria-activedescendant') {
          updateVirtualFocus();
        }
      }
    });
    
    observer.observe(document.body, {
      attributes: true,
      subtree: true,
      attributeFilter: ['aria-activedescendant']
    });
    
    updateVirtualFocus();
    
    return () => {
      document.removeEventListener('focusin', updateVirtualFocus);
      document.removeEventListener('focusout', updateVirtualFocus);
      observer.disconnect();
    };
  }, []);
  
  return (
    <div>
      <p>Currently virtually focused element:</p>
      <pre>{virtuallyFocused ? virtuallyFocused.outerHTML : 'None'}</pre>
    </div>
  );
}

// Focus synchronization
function FocusSynchronizer({ onVirtualFocusChange }) {
  useEffect(() => {
    const checkVirtualFocus = () => {
      const element = getVirtuallyFocusedElement(document);
      onVirtualFocusChange(element);
    };
    
    // Polling approach for demonstration
    const interval = setInterval(checkVirtualFocus, 100);
    
    return () => clearInterval(interval);
  }, [onVirtualFocusChange]);
  
  return null;
}

Virtual Focus Patterns

aria-activedescendant Pattern

The virtual focus system supports the aria-activedescendant pattern where:

  • A container element maintains actual focus
  • A descendant element is marked as "active" via aria-activedescendant
  • Virtual focus events are dispatched on the active descendant
  • Screen readers announce the active descendant as if it has focus

Common Use Cases

Listbox/Combobox:

  • Container input or div has actual focus
  • Options are marked as active via aria-activedescendant
  • Arrow keys change which option is virtually focused

Grid/TreeGrid:

  • Grid container has actual focus
  • Individual cells are virtually focused via aria-activedescendant
  • Arrow keys navigate between cells

Menu/Menubar:

  • Menu has actual focus
  • Menu items are virtually focused
  • Arrow keys and letters navigate items

Event Dispatching

Virtual focus events are regular DOM events:

  • focus event with relatedTarget set to previous element
  • blur event with relatedTarget set to next element
  • focusin and focusout events that bubble normally
  • Events can be prevented with preventDefault()

Screen Reader Support

Virtual focus is announced by screen readers when:

  • The virtually focused element has proper ARIA roles
  • The container has aria-activedescendant pointing to the virtual element
  • The virtual element has appropriate labels and descriptions
  • Focus events are properly dispatched for screen reader detection

Performance Considerations

  • Virtual focus avoids the performance cost of moving actual DOM focus
  • Useful for large lists where moving focus would cause scrolling issues
  • Reduces layout thrashing in complex UI components
  • Allows custom focus styling without browser focus ring limitations

Install with Tessl CLI

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

docs

focus-navigation.md

focus-ring.md

focus-scope.md

index.md

virtual-focus.md

tile.json