Skip to content

[Feature Request]: Add anchor scroll support inside overflowing elements #87

@hirasso

Description

@hirasso

Describe the problem 🧐

  • Currently, scroll plugin only supports scrolling the Window
  • The underlying library gmrchk/scrl has that limitation as well
  • More complex scenarios like overlays or other overflowing divs are currently not supported

Describe the propsed solution 😎

  • Fork gmrchk/scrl to swup/scrl OR implement the logic directly in ScrollPlugin
  • Implement support for scrolling to anchor elements inside overflowing divs
  • Pass the scroll root to swup.scrollTo()

Example implementation

/**
 * Find the closest overflowing parent
 */
export function getScrollableParent(
  element: Element | null | undefined,
): HTMLElement | null {
  if (!element) return null;

  let parent: HTMLElement | null = element.parentElement;

  while (parent) {
    const { overflowY } = getComputedStyle(parent);
    const isScrollable =
      ["auto", "scroll"].includes(overflowY) &&
      parent.scrollHeight > parent.clientHeight;

    if (isScrollable) {
      return parent;
    }

    parent = parent.parentElement;
  }

  return document.documentElement;
}
/**
 * Attempt to scroll to an anchor, inside an overflowing element or the Window
 */
export function maybeScrollToAnchor(
  swup: Swup,
  hash?: string,
  animate: boolean = false,
): boolean {
  if (!hash) {
    return false;
  }
  const scrollPlugin = swup.findPlugin("SwupScrollPlugin") as SwupScrollPlugin;
  const element = scrollPlugin.getAnchorElement(hash);

  if (!element) {
    console.warn(`Anchor target ${hash} not found`);
    return false;
  }

  if (!(element instanceof Element)) {
    console.warn(`Anchor target ${hash} is not a DOM node`);
    return false;
  }

  const { top: elementTop } = element.getBoundingClientRect();
  const scrollRoot = getScrollableParent(element)!;
  const scrollTop = scrollRoot.scrollTop;

  const y = elementTop + scrollTop - scrollPlugin.getOffset(element);
  // swup.scrollTo(scrollRoot, y, animate);
  scrollToAnchor(swup, scrollRoot, y, animate);

  return true;
}

/**
 * Scroll to an anchor inside an element
 */
export function scrollToAnchor(
  swup: Swup,
  target: HTMLElement,
  y: number,
  animate: boolean,
): void {
  if (!animate) {
    swup.hooks.callSync("scroll:start", undefined);
    target.scrollTo(0, y);
    swup.hooks.callSync("scroll:end", undefined);
    return;
  }

  /**
   * Use GSAP ScrollToPlugin for animated scrolling
   * @see https://greensock.com/docs/v3/Plugins/ScrollToPlugin
   */
  gsap.to(target, {
    duration: 0.6,
    scrollTo: {
      y,
      autoKill: !isTouch(),
    },
    ease: "scrl1",
    onStart: () => {
      swup.hooks.callSync("scroll:start", undefined);
    },
    onComplete: () => {
      swup.hooks.callSync("scroll:end", undefined);
    },
    onAutoKill: () => {
      swup.hooks.callSync("scroll:end", undefined);
    },
  });
}

Alternatives considered 🤔

Keep hacking this in every new project.

Considerations

This would be a breaking change since swup.scrollTo() would now need to know what to scroll (The Window or an element).

How important is this feature to you? 🧭

Would make my life a lot easier

Checked all these? 📚

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions