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

scrolling-and-layout.mddocs/

Scrolling & Layout

Viewport tracking, scroll utilities, element positioning, and resize observation for responsive React components.

Capabilities

Scroll Parent Detection

Utilities for finding scrollable ancestors and determining scroll behavior.

/**
 * Finds the nearest scrollable ancestor element
 * @param node - Starting element
 * @param checkForOverflow - Whether to check overflow styles (default: true)
 * @returns Scrollable parent element or document scrolling element
 */
function getScrollParent(node: Element, checkForOverflow?: boolean): Element;

/**
 * Gets array of all scrollable ancestors
 * @param node - Starting element  
 * @returns Array of scrollable parent elements
 */
function getScrollParents(node: Element): Element[];

/**
 * Determines if element is scrollable
 * @param element - Element to check
 * @param checkForOverflow - Whether to check overflow styles
 * @returns true if element can scroll
 */
function isScrollable(element: Element, checkForOverflow?: boolean): boolean;

Usage Examples:

import { getScrollParent, getScrollParents, isScrollable } from "@react-aria/utils";

function ScrollAwareComponent() {
  const elementRef = useRef<HTMLDivElement>(null);
  const [scrollParent, setScrollParent] = useState<Element | null>(null);
  
  useEffect(() => {
    if (elementRef.current) {
      // Find immediate scroll parent
      const parent = getScrollParent(elementRef.current);
      setScrollParent(parent);
      
      // Get all scroll parents for complex layouts
      const allParents = getScrollParents(elementRef.current);
      console.log('All scroll parents:', allParents);
      
      // Check if element itself is scrollable
      const canScroll = isScrollable(elementRef.current);
      console.log('Element can scroll:', canScroll);
    }
  }, []);
  
  return (
    <div ref={elementRef}>
      Content that needs scroll awareness
      {scrollParent && (
        <p>Scroll parent: {scrollParent.tagName}</p>
      )}
    </div>
  );
}

// Sticky positioning with scroll parent awareness
function StickyHeader() {
  const headerRef = useRef<HTMLElement>(null);
  const [isSticky, setIsSticky] = useState(false);
  
  useEffect(() => {
    if (!headerRef.current) return;
    
    const scrollParent = getScrollParent(headerRef.current);
    
    const handleScroll = () => {
      const scrollTop = scrollParent.scrollTop || 0;
      setIsSticky(scrollTop > 100);
    };
    
    scrollParent.addEventListener('scroll', handleScroll, { passive: true });
    return () => scrollParent.removeEventListener('scroll', handleScroll);
  }, []);
  
  return (
    <header 
      ref={headerRef}
      className={isSticky ? 'sticky' : ''}
    >
      Header Content
    </header>
  );
}

Scroll Into View

Utilities for scrolling elements into view with smart positioning.

/**
 * Scrolls container so element is visible (like {block: 'nearest'})
 * @param scrollView - Container element to scroll
 * @param element - Element to bring into view
 */
function scrollIntoView(scrollView: HTMLElement, element: HTMLElement): void;

/**
 * Scrolls element into viewport with overlay awareness
 * @param targetElement - Element to scroll into view
 * @param opts - Options for scrolling behavior
 */
function scrollIntoViewport(
  targetElement: Element, 
  opts?: { containingElement?: Element }
): void;

Usage Examples:

import { scrollIntoView, scrollIntoViewport } from "@react-aria/utils";

function ScrollableList({ items, selectedIndex }) {
  const listRef = useRef<HTMLUListElement>(null);
  const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
  
  // Scroll selected item into view when selection changes
  useEffect(() => {
    if (selectedIndex >= 0 && itemRefs.current[selectedIndex] && listRef.current) {
      scrollIntoView(listRef.current, itemRefs.current[selectedIndex]!);
    }
  }, [selectedIndex]);
  
  return (
    <ul ref={listRef} className="scrollable-list">
      {items.map((item, index) => (
        <li
          key={item.id}
          ref={el => itemRefs.current[index] = el}
          className={index === selectedIndex ? 'selected' : ''}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

// Modal with smart scrolling
function Modal({ isOpen, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (isOpen && modalRef.current) {
      // Scroll modal into viewport, accounting for overlays
      scrollIntoViewport(modalRef.current);
    }
  }, [isOpen]);
  
  return isOpen ? (
    <div className="modal-backdrop">
      <div ref={modalRef} className="modal-content">
        {children}
      </div>
    </div>
  ) : null;
}

// Keyboard navigation with scroll
function useKeyboardNavigation(items: any[], onSelect: (index: number) => void) {
  const [selectedIndex, setSelectedIndex] = useState(0);
  const containerRef = useRef<HTMLElement>(null);
  
  const handleKeyDown = useCallback((e: KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setSelectedIndex(prev => {
          const newIndex = Math.min(prev + 1, items.length - 1);
          
          // Scroll item into view
          const container = containerRef.current;
          const item = container?.children[newIndex] as HTMLElement;
          if (container && item) {
            scrollIntoView(container, item);
          }
          
          return newIndex;
        });
        break;
        
      case 'ArrowUp':
        e.preventDefault();
        setSelectedIndex(prev => {
          const newIndex = Math.max(prev - 1, 0);
          
          const container = containerRef.current;
          const item = container?.children[newIndex] as HTMLElement;
          if (container && item) {
            scrollIntoView(container, item);
          }
          
          return newIndex;
        });
        break;
        
      case 'Enter':
        e.preventDefault();
        onSelect(selectedIndex);
        break;
    }
  }, [items.length, selectedIndex, onSelect]);
  
  return { selectedIndex, containerRef, handleKeyDown };
}

Viewport Size Tracking

Hook for tracking viewport dimensions with device-aware updates.

/**
 * Tracks viewport dimensions with device-aware updates
 * @returns Object with current viewport width and height
 */
function useViewportSize(): ViewportSize;

interface ViewportSize {
  width: number;
  height: number;
}

Usage Examples:

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

function ResponsiveComponent() {
  const { width, height } = useViewportSize();
  
  const isMobile = width < 768;
  const isTablet = width >= 768 && width < 1024;
  const isDesktop = width >= 1024;
  
  return (
    <div>
      <p>Viewport: {width} x {height}</p>
      <div className={`layout ${isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop'}`}>
        {isMobile ? (
          <MobileLayout />
        ) : isTablet ? (
          <TabletLayout />
        ) : (
          <DesktopLayout />
        )}
      </div>
    </div>
  );
}

// Responsive grid based on viewport
function ResponsiveGrid({ children }) {
  const { width } = useViewportSize();
  
  const columns = useMemo(() => {
    if (width < 480) return 1;
    if (width < 768) return 2;
    if (width < 1024) return 3;
    return 4;
  }, [width]);
  
  return (
    <div 
      style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${columns}, 1fr)`,
        gap: '1rem'
      }}
    >
      {children}
    </div>
  );
}

// Virtual keyboard handling on mobile
function MobileForm() {
  const { height } = useViewportSize();
  const [initialHeight] = useState(() => window.innerHeight);
  
  // Detect virtual keyboard
  const isVirtualKeyboardOpen = height < initialHeight * 0.8;
  
  return (
    <form className={isVirtualKeyboardOpen ? 'keyboard-open' : ''}>
      <input placeholder="This adjusts for virtual keyboard" />
      <button type="submit">Submit</button>
    </form>
  );
}

Resize Observer

Hook for observing element resize with ResizeObserver API and fallbacks.

/**
 * Observes element resize with ResizeObserver API
 * @param options - Configuration for resize observation
 */
function useResizeObserver<T extends Element>(options: {
  ref: RefObject<T>;
  box?: ResizeObserverBoxOptions;
  onResize: () => void;
}): void;

type ResizeObserverBoxOptions = "border-box" | "content-box" | "device-pixel-content-box";

Usage Examples:

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

function ResizableComponent() {
  const elementRef = useRef<HTMLDivElement>(null);
  const [size, setSize] = useState({ width: 0, height: 0 });
  
  useResizeObserver({
    ref: elementRef,
    onResize: () => {
      if (elementRef.current) {
        const { offsetWidth, offsetHeight } = elementRef.current;
        setSize({ width: offsetWidth, height: offsetHeight });
      }
    }
  });
  
  return (
    <div ref={elementRef} style={{ resize: 'both', overflow: 'auto', border: '1px solid #ccc' }}>
      <p>Resize me!</p>
      <p>Current size: {size.width} x {size.height}</p>
    </div>
  );
}

// Responsive text sizing based on container
function ResponsiveText({ children }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [fontSize, setFontSize] = useState(16);
  
  useResizeObserver({
    ref: containerRef,
    onResize: () => {
      if (containerRef.current) {
        const width = containerRef.current.offsetWidth;
        // Scale font size based on container width
        const newFontSize = Math.max(12, Math.min(24, width / 20));
        setFontSize(newFontSize);
      }
    }
  });
  
  return (
    <div ref={containerRef} style={{ fontSize: `${fontSize}px` }}>
      {children}
    </div>
  );
}

// Chart that redraws on resize
function Chart({ data }) {
  const chartRef = useRef<HTMLCanvasElement>(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  
  useResizeObserver({
    ref: chartRef,
    box: 'content-box',
    onResize: () => {
      if (chartRef.current) {
        const { clientWidth, clientHeight } = chartRef.current;
        setDimensions({ width: clientWidth, height: clientHeight });
      }
    }
  });
  
  // Redraw chart when dimensions change
  useEffect(() => {
    if (chartRef.current && dimensions.width > 0) {
      drawChart(chartRef.current, data, dimensions);
    }
  }, [data, dimensions]);
  
  return <canvas ref={chartRef} />;
}

Advanced Layout Patterns

Complex scrolling and layout scenarios:

import { 
  useViewportSize, 
  useResizeObserver, 
  scrollIntoView, 
  getScrollParents 
} from "@react-aria/utils";

function InfiniteScrollList({ items, onLoadMore }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const sentinelRef = useRef<HTMLDivElement>(null);
  const { height: viewportHeight } = useViewportSize();
  
  // Resize handling for container
  useResizeObserver({
    ref: containerRef,
    onResize: () => {
      // Recalculate visible items when container resizes
      updateVisibleItems();
    }
  });
  
  // Intersection observer for infinite scroll
  useEffect(() => {
    if (!sentinelRef.current) return;
    
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          onLoadMore();
        }
      },
      { threshold: 0.1 }
    );
    
    observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [onLoadMore]);
  
  // Smart scrolling for keyboard navigation
  const scrollToItem = useCallback((index: number) => {
    const container = containerRef.current;
    const item = container?.children[index] as HTMLElement;
    
    if (container && item) {
      scrollIntoView(container, item);
    }
  }, []);
  
  return (
    <div 
      ref={containerRef}
      style={{ height: Math.min(viewportHeight * 0.8, 600), overflow: 'auto' }}
    >
      {items.map((item, index) => (
        <div key={item.id}>
          {item.content}
        </div>
      ))}
      <div ref={sentinelRef} style={{ height: 1 }} />
    </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