CxJS

findScrollableParent

import { findScrollableParent } from 'cx/util'; Copied

The findScrollableParent function finds the nearest ancestor element that has scrollable overflow.

Basic Usage

import { findScrollableParent } from "cx/util";

const element = document.querySelector(".item");

// Find vertically scrollable parent
const scrollParent = findScrollableParent(element);

// Find horizontally scrollable parent
const hScrollParent = findScrollableParent(element, true);

How It Works

The function traverses up the DOM tree looking for an element where:

  1. clientHeight < scrollHeight (for vertical) or clientWidth < scrollWidth (for horizontal)
  2. The computed overflow-y or overflow-x is "auto" or "scroll"

If no scrollable parent is found, it returns the document’s scrolling element.

Examples

Detecting Scroll Container

import { findScrollableParent } from "cx/util";

function getScrollContainer(element: Element): HTMLElement {
  return findScrollableParent(element) || document.documentElement;
}

Manual Scroll Control

import { findScrollableParent } from "cx/util";

function scrollToTop(element: Element): void {
  const scrollParent = findScrollableParent(element);
  if (scrollParent) {
    scrollParent.scrollTop = 0;
  }
}

function scrollToBottom(element: Element): void {
  const scrollParent = findScrollableParent(element);
  if (scrollParent) {
    scrollParent.scrollTop = scrollParent.scrollHeight;
  }
}

Infinite Scroll Detection

import { findScrollableParent } from "cx/util";

function setupInfiniteScroll(
  container: Element,
  loadMore: () => void
): () => void {
  const scrollParent = findScrollableParent(container);
  if (!scrollParent) return () => {};

  function handleScroll() {
    const { scrollTop, scrollHeight, clientHeight } = scrollParent;
    const nearBottom = scrollTop + clientHeight >= scrollHeight - 100;
    if (nearBottom) {
      loadMore();
    }
  }

  scrollParent.addEventListener("scroll", handleScroll);
  return () => scrollParent.removeEventListener("scroll", handleScroll);
}

Horizontal Scrolling

import { findScrollableParent } from "cx/util";

function scrollHorizontally(element: Element, delta: number): void {
  const scrollParent = findScrollableParent(element, true);
  if (scrollParent) {
    scrollParent.scrollLeft += delta;
  }
}

Common Use Cases

import { findScrollableParent } from "cx/util";

function positionDropdown(trigger: Element, dropdown: HTMLElement): void {
  const scrollParent = findScrollableParent(trigger);
  const triggerRect = trigger.getBoundingClientRect();
  const scrollRect = scrollParent?.getBoundingClientRect();

  // Check if there's room below
  const spaceBelow = scrollRect
    ? scrollRect.bottom - triggerRect.bottom
    : window.innerHeight - triggerRect.bottom;

  if (spaceBelow < dropdown.offsetHeight) {
    // Position above instead
    dropdown.style.bottom = `${triggerRect.height}px`;
  }
}

Scroll Event Binding

import { findScrollableParent } from "cx/util";

function onScroll(element: Element, callback: () => void): () => void {
  const scrollParent = findScrollableParent(element);
  if (!scrollParent) return () => {};

  scrollParent.addEventListener("scroll", callback);
  return () => scrollParent.removeEventListener("scroll", callback);
}

API

function findScrollableParent(
  sourceEl: Element,
  horizontal?: boolean
): HTMLElement | null;
ParameterTypeDefaultDescription
sourceElElement-The element to start searching from
horizontalbooleanfalseSearch for horizontal scroll instead of vertical

Returns: The nearest scrollable ancestor, or the document’s scrolling element if none found.