SheetXL - v0.8.0-beta.9+sites.210.e2d420901
    Preparing search index...

    Per-axis routing over (up to) two Viewports so one viewport can bind its horizontal and vertical axes to different controllers. Implements IViewport, so it is interchangeable with a plain Viewport anywhere a viewport is accepted (a grid, a ScrollPane).

    This is what lets grids share scroll WITHOUT sharing both axes:

    • A sheet header follows the main grid's vertical scroll (read-only) while owning its trivial horizontal — _v is the shared viewport (followed), _h is the header's own.
    • Split peers share one vertical viewport (a single rAF ticker ⇒ eased/momentum scroll stays frame-locked) while scrolling horizontally independently — _v shared, _h each their own.

    Horizontal reads/writes route to _h, vertical to _v. When both axes resolve to the SAME viewport (the common case: a standalone grid, or a main grid owning both) it collapses to 1:1 delegation. A FOLLOWED axis is read-only: no setTarget/setBounds — its owner drives it.

    Implements

    Index
    • The raw cell-area client size = the measured client (uncapped by content). The grid reads this for its own pane layout (min(client, content)); getMetrics caps it at content.

      Returns Size

    • The combined viewport (position + measured sizes) as one identity-stable object — what a subscribed ScrollPane reads to size and place its scrollbars.

      Returns ViewportMetrics

    • Convenience { left, top } of the current (rendered) scroll.

      Returns TopLeft

    • The current (rendered) scroll snapshot. The returned object is identity-stable until the scroll changes, so it is safe as a useSyncExternalStore snapshot.

      Returns TopLeft

    • The scroll TARGET the ticker is easing toward (equals current when settled).

      Returns TopLeft

    • The zoom factor (1 = none). Part of the viewport shape so a shared/synced viewport carries it.

      Returns number

    • Whether the viewport has everything it needs to clamp + emit: a measured client (observer fired or manual sizes pushed) AND real content AND a positive client. The shared readiness signal that replaces each grid's "pending until ready" gate.

      Returns boolean

    • Whether the current (rendered) position has reached the target — no easing in progress.

      Returns boolean

    • START measuring: attach a ResizeObserver to el (the scroll-client box) so the viewport owns its own client-size measurement and derives its clamp bounds internally. Returns an unobserve fn — symmetric with subscribe/unsubscribe, and the ONLY teardown the viewport has (there is no dispose; the rAF ticker is self-terminating and listeners each remove themselves).

      Parameters

      • el: Element | null

      Returns () => void

      Each call is SELF-CONTAINED (its own observer, its own detach), so multiple elements can observe the same viewport concurrently — e.g. split peers each reporting the shared axis (they report the same value by construction; last write wins). A caller tears down exactly what it set up, nothing else — StrictMode mount→cleanup→mount is naturally symmetric. Guarded for absence exactly like the rAF ticker: with no ResizeObserver (SSR / non-DOM tests) it resolves an immediate default client so isReady still becomes true. null is a no-op (returns a no-op detach).

    • Relay a zoom-gesture step to the zoom's owner. The ScrollPane that runs the gesture calls this (as it calls setTarget for a scroll); the viewport does NOT apply it — zoom is a whole percent owned by a model the viewport knows nothing about — it hands the step to subscribeZoom listeners, who commit it and push the result back in through setZoom.

      Parameters

      Returns void

    • Set the clamp bounds (structural — total size, viewport size, frozen floors). Re-clamps the current target into the new bounds (e.g. the viewport shrank) and eases to it. Pushed by the grid; the controller, not the grid, owns the clamp.

      Parameters

      Returns void

    • Declare the content geometry (the host — a grid — pushes this): the total scroll extent plus the frozen-pane floors (min*, the same minLeft/minTop a manual setBounds carries). Calling it switches the viewport into DERIVED-bounds mode — it computes its own bounds from content − client and no longer needs the host to push setBounds. Emits + re-derives.

      Parameters

      • totalWidth: number
      • totalHeight: number
      • minLeft: number = 0
      • minTop: number = 0

      Returns void

    • Push the measured viewport sizes (the grid measures them where it pushes setBounds; a subscribed ScrollPane reads them via getViewport to size its scrollbar thumbs). Emits on change.

      Parameters

      • width: number
      • height: number
      • totalWidth: number
      • totalHeight: number

      Returns void

      This is the MANUAL input path — used by a standalone ScrollPane driving arbitrary content (no grid to declare setContent). Calling it marks the viewport "manual provenance": the manually pushed sizes/bounds win over anything observe/setContent would derive.

    • Set the scroll TARGET (clamped) and start the ticker if needed.

      Parameters

      • left: number
      • top: number
      • Optionalanimate: boolean

        Animation is a property of the REQUEST, not a global policy: true glides to the target (fixed ~SCROLL_ANIMATION_MS ease-out, long jumps clamped to animate only the arrival — a wheel notch, PageDown, or goto). Omit/false for position control (a thumb drag, the touch scroller's per-frame writes, a model restore) — the classic immediate scroll.

      Returns boolean

      true if the target changed (caller fires its immediate-scroll side effects), else false (a no-op move — caller bails).

    • Set the zoom factor and notify subscribers on a change. Zoom is structural (it changes every pane's geometry), so the grid re-renders when this emits. The scroll position (current + target) is rescaled by the zoom ratio so the same cell stays anchored at the top-left and so scroll / zoom (the zoom-independent base position) is preserved. Because zoom and scroll are mutated together before the single emit, a listener that reads scroll on this emit sees screen px already consistent with the new zoom.

      With left/top (screen px for the NEW zoom) the position is set to them instead of the ratio-rescale, current and target alike, in the same single emit — a focal-point zoom lands zoom and scroll together, with no frame painted at the top-left-anchored position in between.

      Parameters

      • zoom: number
      • Optionalleft: number
      • Optionaltop: number

      Returns void

    • Subscribe to scroll changes. Driven by the rAF ticker, so the listener fires at most once per animation frame (and only while current is advancing toward target). Returns a remover.

      Parameters

      • listener: () => void

      Returns () => void