# schedgrid/dom

URL: https://schedgrid.fabio.cv/docs/dom

Bind a scroll container to the engine and hit-test pointer events.

```ts
import { bindScrollElement, viewportPoint } from 'schedgrid/dom';
```

## bindScrollElement(grid, element, options?) [#bindscrollelementgrid-element-options]

Wires a scroll container to the engine in both directions and returns the
unbind function.

* **element → grid.** `scroll` events become `setScroll` and size changes
  (through `ResizeObserver`) become `setViewport`, both read once up front.
* **grid → element.** When something else moves the grid's scroll (a canvas
  twin handling wheel events, a "scroll to today" button), the element
  follows. The resulting `scroll` event reports the same offset, which
  `setScroll` ignores by value, so the loop closes without a guard flag.

On bind the grid wins: the element is moved to the grid's current scroll
offset rather than asked where it is. That is what makes `createGrid`'s
`scroll` option survive being bound to an element, and what carries the
offset across a rebind onto a fresh element. If the element clamps the write
to its own maximum, the grid adopts the clamped value.

```ts
const unbind = bindScrollElement(grid, scroller);
// later
unbind();
```

**BindScrollOptions**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `resizeObserver` | `{ new (callback: ResizeObserverCallback): ResizeObserver; prototype: ResizeObserver; } \| null \| undefined` | no | Observe the element's size and keep `grid.viewport` in sync. Defaults to `ResizeObserver` when the platform has one; pass `null` to opt out. |

The binder only needs a slice of `HTMLElement`, so it can drive any
scrollable and be stubbed in tests without a DOM:

**ScrollElement**

The slice of `HTMLElement` the binder actually uses, so it can be driven by any scrollable — and stubbed in tests without a DOM.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `clientWidth` | `number` | yes |  |
| `clientHeight` | `number` | yes |  |
| `scrollLeft` | `number` | yes |  |
| `scrollTop` | `number` | yes |  |
| `addEventListener` | `(type: 'scroll', listener: () => void, options?: { passive?: boolean; }) => void` | yes |  |
| `removeEventListener` | `(type: 'scroll', listener: () => void) => void` | yes |  |

## viewportPoint(element, event) [#viewportpointelement-event]

Pointer position relative to the scroll container's padding box, which is
the viewport space `grid.indexAt` and `grid.cellAt` expect.

```ts
scroller.addEventListener('pointermove', (e) => {
  const cell = grid.cellAt(viewportPoint(scroller, e));
  // { row, column } or null outside the content
});
```

Pinned bands are resolved before scroll is applied, so a pointer over the
header row reports the header row wherever the user has scrolled.