# ScrollAreaState

Source: /tuil/docs/reference/packages/scroll/api/scroll-area-state
Locale: en

class exported by @mwillbanks/tuil-scroll.



{/* Generated by tooling/docs/generate-reference.ts. */}

## class [#class]

Public class exported by `@mwillbanks/tuil-scroll`.

```ts
export class ScrollAreaState {
  readonly id: string;
  readonly parentId?: string;
  readonly sticky: Readonly<
    Partial<Record<"top" | "bottom" | "left" | "right", boolean>>
  >;
  readonly followFocus: boolean;
  #viewport: ScrollViewport;
  #extent: ScrollExtent;
  #x = 0;
  #y = 0;
  readonly #observers = new Set<(snapshot: ScrollSnapshot) => void>();

  constructor(options: ScrollAreaOptions) {
    if (!options.id.trim()) throw new Error("Scroll area id cannot be empty");
    this.id = options.id;
    this.parentId = options.parentId;
    this.sticky = Object.freeze({ ...options.sticky });
    this.followFocus = options.followFocus ?? true;
    this.#viewport = this.#normalizeViewport(options.viewport);
    this.#extent = this.#normalizeExtent(options.extent);
    this.#clamp();
  }

  subscribe(observer: (snapshot: ScrollSnapshot) => void): () => void {
    this.#observers.add(observer);
    return () => this.#observers.delete(observer);
  }

  snapshot(): ScrollSnapshot {
    const maximumX = this.#maxX();
    const maximumY = this.#maxY();
    return Object.freeze({
      id: this.id,
      position: Object.freeze({ x: this.#x, y: this.#y }),
      viewport: Object.freeze({ ...this.#viewport }),
      extent: Object.freeze({ ...this.#extent }),
      atTop: this.#y === 0,
      atBottom: this.#y === maximumY,
      atLeft: this.#x === 0,
      atRight: this.#x === maximumX,
    });
  }

  resize(viewport: ScrollViewport, extent = this.#extent): void {
    const before = this.snapshot();
    this.#viewport = this.#normalizeViewport(viewport);
    this.#extent = this.#normalizeExtent(extent);
    this.#finishExtentChange(before);
  }

  setExtent(
    extent: ScrollExtent,
    options: { readonly insertedBefore?: number } = {},
  ): void {
    const before = this.snapshot();
    this.#extent = this.#normalizeExtent(extent);
    if (options.insertedBefore && !(this.sticky.top && before.atTop)) {
      this.#y += integer(options.insertedBefore);
    }
    this.#finishExtentChange(before);
  }

  scrollTo(position: Partial<ScrollPosition>): ScrollSnapshot {
    if (position.x !== undefined) this.#x = integer(position.x);
    if (position.y !== undefined) this.#y = integer(position.y);
    this.#clamp();
    this.#notify();
    return this.snapshot();
  }

  move(
    direction:
      | "lineUp"
      | "lineDown"
      | "lineLeft"
      | "lineRight"
      | "pageUp"
      | "pageDown"
      | "pageLeft"
      | "pageRight"
      | "top"
      | "bottom"
      | "left"
      | "right",
    amount = 1,
  ): ScrollSnapshot {
    const step = integer(amount, 1);
    const lineDeltas = {
      lineUp: [0, -step],
      lineDown: [0, step],
      lineLeft: [-step, 0],
      lineRight: [step, 0],
      pageUp: [0, -this.#viewport.height * step],
      pageDown: [0, this.#viewport.height * step],
      pageLeft: [-this.#viewport.width * step, 0],
      pageRight: [this.#viewport.width * step, 0],
    } as const;
    const delta = lineDeltas[direction as keyof typeof lineDeltas];
    if (delta) [this.#x, this.#y] = [this.#x + delta[0], this.#y + delta[1]];
    else this.#moveToEdge(direction);
    this.#clamp();
    this.#notify();
    return this.snapshot();
  }

  wheel(deltaX: number, deltaY: number): ScrollSnapshot {
    return this.scrollTo({ x: this.#x + deltaX, y: this.#y + deltaY });
  }

  scrollIntoView(
    bounds: {
      readonly x: number;
      readonly y: number;
      readonly width: number;
      readonly height: number;
    },
    alignment: ScrollAlignment = "nearest",
  ): ScrollSnapshot {
    const align = (
      start: number,
      size: number,
      offset: number,
      viewportSize: number,
    ): number => {
      if (alignment === "start") return start;
      if (alignment === "center")
        return start - Math.floor((viewportSize - size) / 2);
      if (alignment === "end") return start + size - viewportSize;
      if (start < offset) return start;
      if (start + size > offset + viewportSize)
        return start + size - viewportSize;
      return offset;
    };
    return this.scrollTo({
      x: align(bounds.x, bounds.width, this.#x, this.#viewport.width),
      y: align(bounds.y, bounds.height, this.#y, this.#viewport.height),
    });
  }

  visibleRange(
    axis: ScrollAxis,
    measurements: readonly number[],
    overscan = 0,
  ): {
    readonly start: number;
    readonly end: number;
    readonly before: number;
    readonly after: number;
  } {
    const sizes = normalizeMeasurements(measurements);
    const offset = axis === "vertical" ? this.#y : this.#x;
    const size =
      axis === "vertical" ? this.#viewport.height : this.#viewport.width;
    const { start, end } = visibleMeasurementInterval(sizes, offset, size);
    const safeOverscan = integer(overscan);
    const first = Math.max(0, start - safeOverscan);
    const last = Math.min(
      sizes.length - 1,
      Math.max(start, end - 1 + safeOverscan),
    );
    const before = sizes.slice(0, first).reduce((sum, value) => sum + value, 0);
    const included = sizes
      .slice(first, last + 1)
      .reduce((sum, value) => sum + value, 0);
    const total = sizes.reduce((sum, value) => sum + value, 0);
    return Object.freeze({
      start: first,
      end: last,
      before,
      after: Math.max(0, total - before - included),
    });
  }

  #finishExtentChange(before: ScrollSnapshot): void {
    if (this.sticky.top && before.atTop) this.#y = 0;
    if (this.sticky.bottom && before.atBottom) this.#y = this.#maxY();
    if (this.sticky.left && before.atLeft) this.#x = 0;
    if (this.sticky.right && before.atRight) this.#x = this.#maxX();
    this.#clamp();
    this.#notify();
  }

  #moveToEdge(direction: string): void {
    const positions: Readonly<Record<string, readonly [number, number]>> = {
      top: [this.#x, 0],
      bottom: [this.#x, this.#maxY()],
      left: [0, this.#y],
      right: [this.#maxX(), this.#y],
    };
    const position = positions[direction];
    if (position) [this.#x, this.#y] = position;
  }

  staticProjection(
    content: readonly string[],
    mode: "all" | "viewport" = "all",
  ): readonly string[] {
    if (mode === "all") return Object.freeze([...content]);
    return Object.freeze(
      content.slice(this.#y, this.#y + this.#viewport.height),
    );
  }

  #normalizeViewport(viewport: ScrollViewport): ScrollViewport {
    return Object.freeze({
      width: integer(viewport.width, 1),
      height: integer(viewport.height, 1),
    });
  }

  #normalizeExtent(extent: ScrollExtent): ScrollExtent {
    return Object.freeze({
      width: integer(extent.width),
      height: integer(extent.height),
    });
  }

  #maxX(): number {
    return Math.max(0, this.#extent.width - this.#viewport.width);
  }

  #maxY(): number {
    return Math.max(0, this.#extent.height - this.#viewport.height);
  }

  #clamp(): void {
    this.#x = Math.max(0, Math.min(this.#maxX(), this.#x));
    this.#y = Math.max(0, Math.min(this.#maxY(), this.#y));
  }

  #notify(): void {
    const snapshot = this.snapshot();
    for (const observer of this.#observers) observer(snapshot);
  }
}
```

## Members [#members]

| Member                | Type                                                                                                                                                                                                   | Required | Description                                                                                                                                                                                                                                 | Related types                                                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | `string`                                                                                                                                                                                               | Yes      | The `id` member uses the `string` contract.                                                                                                                                                                                                 | —                                                                                                                                                  |
| `parentId`            | `string \| undefined`                                                                                                                                                                                  | No       | The `parentId` member uses the `string \| undefined` contract.                                                                                                                                                                              | —                                                                                                                                                  |
| `sticky`              | `Readonly<Partial<Record<"top" \| "bottom" \| "left" \| "right", boolean>>>`                                                                                                                           | Yes      | The `sticky` member uses the `Readonly<Partial<Record<"top" \| "bottom" \| "left" \| "right", boolean>>>` contract.                                                                                                                         | —                                                                                                                                                  |
| `followFocus`         | `boolean`                                                                                                                                                                                              | Yes      | The `followFocus` member uses the `boolean` contract.                                                                                                                                                                                       | —                                                                                                                                                  |
| `#viewport`           | `ScrollViewport`                                                                                                                                                                                       | Yes      | The `#viewport` member uses the `ScrollViewport` contract.                                                                                                                                                                                  | [`ScrollViewport`](/tuil/docs/reference/packages/scroll/api/scroll-viewport)                                                                            |
| `#extent`             | `ScrollExtent`                                                                                                                                                                                         | Yes      | The `#extent` member uses the `ScrollExtent` contract.                                                                                                                                                                                      | [`ScrollExtent`](/tuil/docs/reference/packages/scroll/api/scroll-extent)                                                                                |
| `#x`                  | `number`                                                                                                                                                                                               | Yes      | The `#x` member uses the `number` contract.                                                                                                                                                                                                 | —                                                                                                                                                  |
| `#y`                  | `number`                                                                                                                                                                                               | Yes      | The `#y` member uses the `number` contract.                                                                                                                                                                                                 | —                                                                                                                                                  |
| `#observers`          | `Set<(snapshot: ScrollSnapshot) => void>`                                                                                                                                                              | Yes      | The `#observers` member uses the `Set<(snapshot: ScrollSnapshot) => void>` contract.                                                                                                                                                        | [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)                                                                            |
| `__constructor`       | `any`                                                                                                                                                                                                  | Yes      | The `__constructor` member uses the `any` contract.                                                                                                                                                                                         | —                                                                                                                                                  |
| `subscribe`           | `(observer: (snapshot: ScrollSnapshot) => void) => () => void`                                                                                                                                         | Yes      | The `subscribe` member uses the `(observer: (snapshot: ScrollSnapshot) => void) => () => void` contract.                                                                                                                                    | [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)                                                                            |
| `snapshot`            | `() => ScrollSnapshot`                                                                                                                                                                                 | Yes      | The `snapshot` member uses the `() => ScrollSnapshot` contract.                                                                                                                                                                             | [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)                                                                            |
| `resize`              | `(viewport: ScrollViewport, extent?: ScrollExtent) => void`                                                                                                                                            | Yes      | The `resize` member uses the `(viewport: ScrollViewport, extent?: ScrollExtent) => void` contract.                                                                                                                                          | [`ScrollExtent`](/tuil/docs/reference/packages/scroll/api/scroll-extent), [`ScrollViewport`](/tuil/docs/reference/packages/scroll/api/scroll-viewport)       |
| `setExtent`           | `(extent: ScrollExtent, options?: &#123; readonly insertedBefore?: number; &#125;) => void`                                                                                                            | Yes      | The `setExtent` member uses the `(extent: ScrollExtent, options?: &#123; readonly insertedBefore?: number; &#125;) => void` contract.                                                                                                       | [`ScrollExtent`](/tuil/docs/reference/packages/scroll/api/scroll-extent)                                                                                |
| `scrollTo`            | `(position: Partial<ScrollPosition>) => ScrollSnapshot`                                                                                                                                                | Yes      | The `scrollTo` member uses the `(position: Partial<ScrollPosition>) => ScrollSnapshot` contract.                                                                                                                                            | [`ScrollPosition`](/tuil/docs/reference/packages/scroll/api/scroll-position), [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)   |
| `move`                | `(direction: "lineUp" \| "lineDown" \| "lineLeft" \| "lineRight" \| "pageUp" \| "pageDown" \| "pageLeft" \| "pageRight" \| "top" \| "bottom" \| "left" \| "right", amount?: number) => ScrollSnapshot` | Yes      | The `move` member uses the `(direction: "lineUp" \| "lineDown" \| "lineLeft" \| "lineRight" \| "pageUp" \| "pageDown" \| "pageLeft" \| "pageRight" \| "top" \| "bottom" \| "left" \| "right", amount?: number) => ScrollSnapshot` contract. | [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)                                                                            |
| `wheel`               | `(deltaX: number, deltaY: number) => ScrollSnapshot`                                                                                                                                                   | Yes      | The `wheel` member uses the `(deltaX: number, deltaY: number) => ScrollSnapshot` contract.                                                                                                                                                  | [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)                                                                            |
| `scrollIntoView`      | `(bounds: &#123; readonly x: number; readonly y: number; readonly width: number; readonly height: number; &#125;, alignment?: ScrollAlignment) => ScrollSnapshot`                                      | Yes      | The `scrollIntoView` member uses the `(bounds: &#123; readonly x: number; readonly y: number; readonly width: number; readonly height: number; &#125;, alignment?: ScrollAlignment) => ScrollSnapshot` contract.                            | [`ScrollAlignment`](/tuil/docs/reference/packages/scroll/api/scroll-alignment), [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot) |
| `visibleRange`        | `(axis: ScrollAxis, measurements: readonly number[], overscan?: number) => &#123; readonly start: number; readonly end: number; readonly before: number; readonly after: number; &#125;`               | Yes      | The `visibleRange` member uses the `(axis: ScrollAxis, measurements: readonly number[], overscan?: number) => &#123; readonly start: number; readonly end: number; readonly before: number; readonly after: number; &#125;` contract.       | [`ScrollAxis`](/tuil/docs/reference/packages/scroll/api/scroll-axis)                                                                                    |
| `#finishExtentChange` | `(before: ScrollSnapshot) => void`                                                                                                                                                                     | Yes      | The `#finishExtentChange` member uses the `(before: ScrollSnapshot) => void` contract.                                                                                                                                                      | [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)                                                                            |
| `#moveToEdge`         | `(direction: string) => void`                                                                                                                                                                          | Yes      | The `#moveToEdge` member uses the `(direction: string) => void` contract.                                                                                                                                                                   | —                                                                                                                                                  |
| `staticProjection`    | `(content: readonly string[], mode?: "all" \| "viewport") => readonly string[]`                                                                                                                        | Yes      | The `staticProjection` member uses the `(content: readonly string[], mode?: "all" \| "viewport") => readonly string[]` contract.                                                                                                            | —                                                                                                                                                  |
| `#normalizeViewport`  | `(viewport: ScrollViewport) => ScrollViewport`                                                                                                                                                         | Yes      | The `#normalizeViewport` member uses the `(viewport: ScrollViewport) => ScrollViewport` contract.                                                                                                                                           | [`ScrollViewport`](/tuil/docs/reference/packages/scroll/api/scroll-viewport)                                                                            |
| `#normalizeExtent`    | `(extent: ScrollExtent) => ScrollExtent`                                                                                                                                                               | Yes      | The `#normalizeExtent` member uses the `(extent: ScrollExtent) => ScrollExtent` contract.                                                                                                                                                   | [`ScrollExtent`](/tuil/docs/reference/packages/scroll/api/scroll-extent)                                                                                |
| `#maxX`               | `() => number`                                                                                                                                                                                         | Yes      | The `#maxX` member uses the `() => number` contract.                                                                                                                                                                                        | —                                                                                                                                                  |
| `#maxY`               | `() => number`                                                                                                                                                                                         | Yes      | The `#maxY` member uses the `() => number` contract.                                                                                                                                                                                        | —                                                                                                                                                  |
| `#clamp`              | `() => void`                                                                                                                                                                                           | Yes      | The `#clamp` member uses the `() => void` contract.                                                                                                                                                                                         | —                                                                                                                                                  |
| `#notify`             | `() => void`                                                                                                                                                                                           | Yes      | The `#notify` member uses the `() => void` contract.                                                                                                                                                                                        | —                                                                                                                                                  |

## Parameters [#parameters]

This declaration has no public members.

## Returns [#returns]

This declaration does not return a value.

## Throws [#throws]

No thrown errors are documented for this declaration.

## Related types [#related-types]

* [`ScrollAlignment`](/tuil/docs/reference/packages/scroll/api/scroll-alignment)
* [`ScrollAreaOptions`](/tuil/docs/reference/packages/scroll/api/scroll-area-options)
* [`ScrollAxis`](/tuil/docs/reference/packages/scroll/api/scroll-axis)
* [`ScrollExtent`](/tuil/docs/reference/packages/scroll/api/scroll-extent)
* [`ScrollPosition`](/tuil/docs/reference/packages/scroll/api/scroll-position)
* [`ScrollSnapshot`](/tuil/docs/reference/packages/scroll/api/scroll-snapshot)
* [`ScrollViewport`](/tuil/docs/reference/packages/scroll/api/scroll-viewport)

## Source [#source]

[View the secondary source reference](https://github.com/mwillbanks/tuil/blob/main/packages/scroll/src/index.ts)

## Package [#package]

[@mwillbanks/tuil-scroll](/tuil/docs/reference/packages/scroll)
