Skip to content

Conversation

@wangtsiao
Copy link

Description

Adds auto-scroll functionality to text selection, enabling seamless multi-page selection when pointer approaches viewport edges.

Changes

  • Auto-scroll triggers within 50-100px of viewport edges during text selection
  • Uses requestAnimationFrame for smooth scrolling
  • Automatically stops when selection ends or pointer moves away
  • Supports all four directional scrolling (up, down, left, right)

Questions

Edge Threshold Configuration

I initially set all edge thresholds (top, bottom, left, right) to 50px.

However, when I was testing with the snippet example, I noticed that 50px wasn't quite enough for the top edge - the auto-scroll wouldn't kick in reliably when I moved my cursor near the top.

As a result, I increased only the top threshold to 100px while keeping the others at 50px:

private readonly AUTO_SCROLL_TOP_THRESHOLD = 100;
private readonly AUTO_SCROLL_BOTTOM_THRESHOLD = 50;
private readonly AUTO_SCROLL_LEFT_THRESHOLD = 50;
private readonly AUTO_SCROLL_RIGHT_THRESHOLD = 50;

This is my first PR here! I'm absolutely loving this project and it's been a pleasure working with the codebase. Please feel free to let me know if there's anything I should improve. Thanks so much for your time! 💙

Automatically scroll viewport when pointer approaches edges during
text selection, enabling seamless multi-page selection.
@vercel
Copy link

vercel bot commented Oct 17, 2025

@wangtsiao is attempting to deploy a commit to the OpenBook Team on Vercel.

A member of the Team first needs to authorize it.

@eposha
Copy link
Contributor

eposha commented Nov 16, 2025

Is it possible to review and merge current PR?

@eposha
Copy link
Contributor

eposha commented Nov 16, 2025

There is currently a PR bug.

The document viewport may not cover the entire user's screen. Therefore, if autoscroll starts and the user moves the mouse outside the document viewport and triggers mouseup, autoscroll will continue to work.

It is better to calculate the position of the document (PDF) viewport and use window.addEventListener(‘pointermove’, function), in which you can calculate how close the user's cursor is to the edge of the document viewport.

Based on your PR, I created a hook that solves this problem.

  const { provides: selection } = useSelectionCapability();
  const { provides: viewport } = useViewportCapability();

  useEffect(() => {
    if (!viewport || !selection) return;

    let autoScrollFrame: number | null = null;
    let lastPointer: { x: number; y: number } | null = null;

    const isSelecting = () => selection.getState().selecting;

    const stop = () => {
      if (autoScrollFrame !== null) {
        cancelAnimationFrame(autoScrollFrame);
        autoScrollFrame = null;
      }
    };

    const computeDir = (x: number, y: number) => {
      // Viewport boundaries in window coordinates
      const rect = viewport.getBoundingRect();
      const vpTop = rect.origin.y;
      const vpLeft = rect.origin.x;
      const vpBottom = vpTop + rect.size.height;
      const vpRight = vpLeft + rect.size.width;

      // Distances from the cursor to the edges of the viewport, not to (0, clientHeight)
      const distanceFromTop = y - vpTop;
      const distanceFromBottom = vpBottom - y;
      const distanceFromLeft = x - vpLeft;
      const distanceFromRight = vpRight - x;

      let scrollX = 0;
      let scrollY = 0;

      if (distanceFromTop >= 0 && distanceFromTop < TOP_T) {
        scrollY = -SPEED; // up
      } else if (distanceFromBottom >= 0 && distanceFromBottom < BOTTOM_T) {
        scrollY = SPEED; // down
      }

      if (distanceFromLeft >= 0 && distanceFromLeft < LEFT_T) {
        scrollX = -SPEED; // left
      } else if (distanceFromRight >= 0 && distanceFromRight < RIGHT_T) {
        scrollX = SPEED; // right
      }

      return { scrollX, scrollY };
    };

    const step = () => {
      if (!lastPointer || !isSelecting()) {
        stop();
        return;
      }

      const { scrollX, scrollY } = computeDir(lastPointer.x, lastPointer.y);

      if (scrollX === 0 && scrollY === 0) {
        stop();
        return;
      }

      const m = viewport.getMetrics();

      viewport.scrollTo({
        x: m.scrollLeft + scrollX,
        y: m.scrollTop + scrollY,
        behavior: 'instant',
      });

      autoScrollFrame = requestAnimationFrame(step);
    };

    const start = () => {
      if (!autoScrollFrame) {
        autoScrollFrame = requestAnimationFrame(step);
      }
    };

    const onMove = (e: PointerEvent) => {
      lastPointer = { x: e.clientX, y: e.clientY };

      if (!isSelecting()) {
        stop();
        return;
      }

      const { scrollX, scrollY } = computeDir(e.clientX, e.clientY);

      if (scrollX !== 0 || scrollY !== 0) {
        start();
      } else {
        stop();
      }
    };

    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', stop);

    return () => {
      stop();
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', stop);
    };
  }, [viewport, selection]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants