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

miscellaneous-utilities.mddocs/

Miscellaneous Utilities

Additional utilities for DOM helpers, value management, constants, and utility functions that support React Aria components.

Capabilities

DOM Helpers

Utilities for working with DOM elements and their properties.

/**
 * Gets element's offset position
 * @param element - Target element
 * @param reverse - Whether to measure from right/bottom (default: false)
 * @param orientation - Measurement direction (default: 'horizontal')
 * @returns Offset value in pixels
 */
function getOffset(
  element: Element, 
  reverse?: boolean, 
  orientation?: "horizontal" | "vertical"
): number;

/**
 * Gets the document that owns an element
 * @param el - Element to get document for
 * @returns Element's owner document or global document
 */
function getOwnerDocument(el: Element): Document;

/**
 * Gets the window that owns an element  
 * @param el - Element to get window for
 * @returns Element's owner window or global window
 */
function getOwnerWindow(el: Element): Window;

/**
 * Type guard to check if node is a ShadowRoot
 * @param node - Node to check
 * @returns Boolean with proper type narrowing
 */
function isShadowRoot(node: Node): node is ShadowRoot;

Usage Examples:

import { getOffset, getOwnerDocument, getOwnerWindow, isShadowRoot } from "@react-aria/utils";

function PositionTracker({ children }) {
  const elementRef = useRef<HTMLDivElement>(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    if (!elementRef.current) return;
    
    const updatePosition = () => {
      const element = elementRef.current!;
      
      // Get horizontal and vertical offsets
      const x = getOffset(element, false, 'horizontal');
      const y = getOffset(element, false, 'vertical');
      
      setPosition({ x, y });
    };
    
    // Get the appropriate window for event listeners
    const ownerWindow = getOwnerWindow(elementRef.current);
    
    updatePosition();
    ownerWindow.addEventListener('scroll', updatePosition);
    ownerWindow.addEventListener('resize', updatePosition);
    
    return () => {
      ownerWindow.removeEventListener('scroll', updatePosition);
      ownerWindow.removeEventListener('resize', updatePosition);
    };
  }, []);
  
  return (
    <div ref={elementRef}>
      <p>Position: ({position.x}, {position.y})</p>
      {children}
    </div>
  );
}

// Cross-frame DOM utilities
function CrossFrameComponent({ targetFrame }) {
  const [targetDocument, setTargetDocument] = useState<Document | null>(null);
  
  useEffect(() => {
    if (targetFrame && targetFrame.contentDocument) {
      const frameDoc = targetFrame.contentDocument;
      setTargetDocument(frameDoc);
      
      // Use owner document utilities for frame elements
      const frameElements = frameDoc.querySelectorAll('.interactive');
      frameElements.forEach(element => {
        const ownerDoc = getOwnerDocument(element);
        const ownerWin = getOwnerWindow(element);
        
        console.log('Element owner document:', ownerDoc === frameDoc);
        console.log('Element owner window:', ownerWin === targetFrame.contentWindow);
      });
    }
  }, [targetFrame]);
  
  return <div>Cross-frame utilities active</div>;
}

// Shadow DOM detection
function ShadowDOMHandler({ rootElement }) {
  useEffect(() => {
    if (!rootElement) return;
    
    const walker = document.createTreeWalker(
      rootElement,
      NodeFilter.SHOW_ELEMENT,
      {
        acceptNode: (node) => {
          // Check if we've encountered a shadow root
          if (isShadowRoot(node)) {
            console.log('Found shadow root:', node);
            return NodeFilter.FILTER_ACCEPT;
          }
          return NodeFilter.FILTER_SKIP;
        }
      }
    );
    
    let currentNode;
    while (currentNode = walker.nextNode()) {
      // Process shadow roots
      handleShadowRoot(currentNode as ShadowRoot);
    }
  }, [rootElement]);
  
  return null;
}

Value Management

Utilities for managing values and preventing unnecessary re-renders.

/**
 * Creates an inert reference to a value
 * @param value - Value to make inert
 * @returns Inert value reference that doesn't trigger re-renders
 */
function inertValue<T>(value: T): T;

Usage Examples:

import { inertValue } from "@react-aria/utils";

function ExpensiveComponent({ data, onProcess }) {
  // Create inert reference to prevent unnecessary recalculations
  const inertData = inertValue(data);
  
  const processedData = useMemo(() => {
    // Expensive processing that should only run when data actually changes
    return expensiveProcessing(inertData);
  }, [inertData]);
  
  return <div>{processedData.summary}</div>;
}

// Stable callback references
function CallbackComponent({ onChange }) {
  // Make callback inert to prevent effect dependencies
  const inertOnChange = inertValue(onChange);
  
  useEffect(() => {
    // This effect won't re-run when onChange reference changes
    const subscription = subscribe(inertOnChange);
    return () => subscription.unsubscribe();
  }, [inertOnChange]);
  
  return <div>Component with stable callback</div>;
}

Constants

Pre-defined constants used by React Aria components.

/**
 * Custom event name for clearing focus in autocomplete
 */
const CLEAR_FOCUS_EVENT = "react-aria-clear-focus";

/**
 * Custom event name for setting focus in autocomplete  
 */
const FOCUS_EVENT = "react-aria-focus";

Usage Examples:

import { CLEAR_FOCUS_EVENT, FOCUS_EVENT } from "@react-aria/utils";

function CustomAutocomplete({ suggestions, onSelect }) {
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);
  
  const clearFocus = () => {
    // Dispatch custom clear focus event
    const event = new CustomEvent(CLEAR_FOCUS_EVENT, {
      bubbles: true,
      detail: { target: inputRef.current }
    });
    
    inputRef.current?.dispatchEvent(event);
  };
  
  const focusOption = (optionElement: HTMLElement) => {
    // Dispatch custom focus event
    const event = new CustomEvent(FOCUS_EVENT, {
      bubbles: true,
      detail: { target: optionElement }
    });
    
    optionElement.dispatchEvent(event);
  };
  
  useEffect(() => {
    const input = inputRef.current;
    if (!input) return;
    
    const handleClearFocus = (e: CustomEvent) => {
      console.log('Clear focus requested:', e.detail.target);
      input.blur();
    };
    
    const handleFocus = (e: CustomEvent) => {
      console.log('Focus requested:', e.detail.target);
      e.detail.target.focus();
    };
    
    input.addEventListener(CLEAR_FOCUS_EVENT, handleClearFocus);
    listRef.current?.addEventListener(FOCUS_EVENT, handleFocus);
    
    return () => {
      input.removeEventListener(CLEAR_FOCUS_EVENT, handleClearFocus);
      listRef.current?.removeEventListener(FOCUS_EVENT, handleFocus);
    };
  }, []);
  
  return (
    <div>
      <input ref={inputRef} />
      <ul ref={listRef}>
        {suggestions.map((suggestion, index) => (
          <li 
            key={index}
            onClick={() => focusOption(inputRef.current!)}
          >
            {suggestion}
          </li>
        ))}
      </ul>
      <button onClick={clearFocus}>Clear Focus</button>
    </div>
  );
}

Drag & Drop (Deprecated)

One-dimensional drag gesture utility (deprecated in favor of newer alternatives).

/**
 * ⚠️ DEPRECATED - Use @react-aria/interactions useMove instead
 * 1D dragging behavior for sliders and similar components
 * @param props - Configuration for drag behavior
 * @returns Event handlers for drag functionality
 */
function useDrag1D(props: {
  containerRef?: RefObject<Element>;
  reverse?: boolean;
  orientation?: "horizontal" | "vertical";
  onDrag?: (e: { deltaX: number; deltaY: number }) => void;
  onDragStart?: (e: PointerEvent) => void;
  onDragEnd?: (e: PointerEvent) => void;
}): HTMLAttributes<HTMLElement>;

Usage Examples:

import { useDrag1D } from "@react-aria/utils";

// ⚠️ This is deprecated - use @react-aria/interactions instead
function DeprecatedSlider({ value, onChange, min = 0, max = 100 }) {
  const containerRef = useRef<HTMLDivElement>(null);
  
  const dragProps = useDrag1D({
    containerRef,
    orientation: 'horizontal',
    onDrag: ({ deltaX }) => {
      const container = containerRef.current;
      if (!container) return;
      
      const containerWidth = container.offsetWidth;
      const deltaValue = (deltaX / containerWidth) * (max - min);
      const newValue = Math.max(min, Math.min(max, value + deltaValue));
      
      onChange(newValue);
    }
  });
  
  return (
    <div ref={containerRef} className="slider" {...dragProps}>
      <div className="slider-track">
        <div 
          className="slider-thumb"
          style={{ left: `${((value - min) / (max - min)) * 100}%` }}
        />
      </div>
    </div>
  );
}

Load More Utilities

Utilities for implementing infinite scrolling and pagination.

/**
 * Manages infinite scrolling and load more functionality
 * @param options - Configuration for load more behavior
 * @returns Load more state and controls
 */
function useLoadMore<T>(options: {
  items: T[];
  onLoadMore: () => Promise<T[]> | void;
  isLoading?: boolean;
  hasMore?: boolean;
}): {
  items: T[];
  isLoading: boolean;
  hasMore: boolean;
  loadMore: () => void;
};

/**
 * Uses IntersectionObserver to trigger load more when sentinel is visible
 * @param props - Configuration for sentinel behavior
 * @param ref - RefObject to sentinel element
 */
function useLoadMoreSentinel(
  props: LoadMoreSentinelProps,
  ref: RefObject<HTMLElement>
): void;

/**
 * Unstable version of useLoadMoreSentinel
 * @deprecated Use useLoadMoreSentinel instead
 */
const UNSTABLE_useLoadMoreSentinel = useLoadMoreSentinel;

interface LoadMoreSentinelProps {
  collection: any;
  onLoadMore: () => void;
  scrollOffset?: number;
}

Usage Examples:

import { useLoadMore, useLoadMoreSentinel } from "@react-aria/utils";

function InfiniteList({ initialItems, loadMoreItems }) {
  const { 
    items, 
    isLoading, 
    hasMore, 
    loadMore 
  } = useLoadMore({
    items: initialItems,
    onLoadMore: async () => {
      const newItems = await loadMoreItems();
      return newItems;
    },
    hasMore: true
  });
  
  const sentinelRef = useRef<HTMLDivElement>(null);
  
  useLoadMoreSentinel({
    collection: items,
    onLoadMore: loadMore,
    scrollOffset: 0.8 // Trigger when 80% scrolled
  }, sentinelRef);
  
  return (
    <div className="infinite-list">
      {items.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      
      {hasMore && (
        <div ref={sentinelRef} className="loading-sentinel">
          {isLoading ? 'Loading...' : 'Load more'}
        </div>
      )}
    </div>
  );
}

Re-exported Utilities

Mathematical utilities re-exported from @react-stately/utils.

/**
 * Clamps value between min and max bounds
 * @param value - Value to clamp
 * @param min - Minimum value
 * @param max - Maximum value  
 * @returns Clamped value
 */
function clamp(value: number, min: number, max: number): number;

/**
 * Snaps value to nearest step increment
 * @param value - Value to snap
 * @param step - Step increment
 * @param min - Optional minimum value
 * @returns Snapped value
 */
function snapValueToStep(value: number, step: number, min?: number): number;

Usage Examples:

import { clamp, snapValueToStep } from "@react-aria/utils";

function NumericInput({ value, onChange, min = 0, max = 100, step = 1 }) {
  const handleChange = (newValue: number) => {
    // Clamp to bounds and snap to step
    const clampedValue = clamp(newValue, min, max);
    const snappedValue = snapValueToStep(clampedValue, step, min);
    
    onChange(snappedValue);
  };
  
  return (
    <input
      type="range"
      value={value}
      min={min}
      max={max}
      step={step}
      onChange={(e) => handleChange(Number(e.target.value))}
    />
  );
}

// Color picker with HSL snapping
function ColorPicker({ hue, saturation, lightness, onChange }) {
  const handleHueChange = (newHue: number) => {
    // Snap hue to 15-degree increments
    const snappedHue = snapValueToStep(newHue, 15);
    const clampedHue = clamp(snappedHue, 0, 360);
    
    onChange({ hue: clampedHue, saturation, lightness });
  };
  
  const handleSaturationChange = (newSaturation: number) => {
    // Snap saturation to 5% increments
    const snappedSat = snapValueToStep(newSaturation, 5);
    const clampedSat = clamp(snappedSat, 0, 100);
    
    onChange({ hue, saturation: clampedSat, lightness });
  };
  
  return (
    <div>
      <input
        type="range"
        min={0}
        max={360}
        step={15}
        value={hue}
        onChange={(e) => handleHueChange(Number(e.target.value))}
      />
      <input
        type="range"
        min={0}
        max={100}
        step={5}
        value={saturation}
        onChange={(e) => handleSaturationChange(Number(e.target.value))}
      />
    </div>
  );
}

// Animation easing with step snapping
function useAnimationValue(targetValue: number, duration = 1000) {
  const [currentValue, setCurrentValue] = useState(0);
  
  useEffect(() => {
    let startTime: number;
    let animationFrame: number;
    
    const animate = (timestamp: number) => {
      if (!startTime) startTime = timestamp;
      
      const progress = clamp((timestamp - startTime) / duration, 0, 1);
      
      // Ease-out function
      const easedProgress = 1 - Math.pow(1 - progress, 3);
      
      // Calculate intermediate value and snap to steps
      const intermediateValue = easedProgress * targetValue;
      const snappedValue = snapValueToStep(intermediateValue, 0.1);
      
      setCurrentValue(snappedValue);
      
      if (progress < 1) {
        animationFrame = requestAnimationFrame(animate);
      }
    };
    
    animationFrame = requestAnimationFrame(animate);
    
    return () => {
      if (animationFrame) {
        cancelAnimationFrame(animationFrame);
      }
    };
  }, [targetValue, duration]);
  
  return currentValue;
}

Utility Combinations

Real-world examples combining multiple miscellaneous utilities:

import { 
  getOffset, 
  getOwnerWindow, 
  clamp, 
  snapValueToStep, 
  inertValue 
} from "@react-aria/utils";

function DraggableSlider({ value, onChange, min = 0, max = 100, step = 1 }) {
  const sliderRef = useRef<HTMLDivElement>(null);
  const [isDragging, setIsDragging] = useState(false);
  
  // Make onChange inert to prevent unnecessary effect re-runs
  const inertOnChange = inertValue(onChange);
  
  const handleMouseMove = useCallback((e: MouseEvent) => {
    if (!isDragging || !sliderRef.current) return;
    
    // Get slider position and dimensions
    const slider = sliderRef.current;
    const sliderRect = slider.getBoundingClientRect();
    const sliderOffset = getOffset(slider, false, 'horizontal');
    
    // Calculate relative position
    const relativeX = e.clientX - sliderRect.left;
    const percentage = clamp(relativeX / sliderRect.width, 0, 1);
    
    // Convert to value and snap to step
    const rawValue = min + percentage * (max - min);
    const snappedValue = snapValueToStep(rawValue, step, min);
    const finalValue = clamp(snappedValue, min, max);
    
    inertOnChange(finalValue);
  }, [isDragging, min, max, step, inertOnChange]);
  
  useEffect(() => {
    if (!isDragging) return;
    
    const ownerWindow = sliderRef.current 
      ? getOwnerWindow(sliderRef.current) 
      : window;
    
    ownerWindow.addEventListener('mousemove', handleMouseMove);
    ownerWindow.addEventListener('mouseup', () => setIsDragging(false));
    
    return () => {
      ownerWindow.removeEventListener('mousemove', handleMouseMove);
      ownerWindow.removeEventListener('mouseup', () => setIsDragging(false));
    };
  }, [isDragging, handleMouseMove]);
  
  const handlePercent = clamp((value - min) / (max - min), 0, 1);
  
  return (
    <div 
      ref={sliderRef}
      className="slider"
      onMouseDown={() => setIsDragging(true)}
    >
      <div 
        className="slider-thumb"
        style={{ left: `${handlePercent * 100}%` }}
      />
    </div>
  );
}

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