@gridkitjs/dom 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blagoja Blazhevski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # @gridkitjs/dom
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The browser behaviours a GridKit renderer needs, as framework-free
4
+ TypeScript: pointer gestures, keyboard focus movement, the observers behind
5
+ fit-to-width and infinite scroll, the row virtualizer's measuring, overlays,
6
+ and a live-region announcer. Built on
7
+ [`@gridkitjs/core`](https://www.npmjs.com/package/@gridkitjs/core) and used by
8
+ [`@gridkitjs/react`](https://www.npmjs.com/package/@gridkitjs/react).
9
+
10
+ > **Early development.** The API is still moving and may break between minor
11
+ > versions.
12
+
13
+ You only need this package to write a renderer of your own. `@gridkitjs/react`
14
+ already depends on it, so installing that is enough to use the grid.
15
+
16
+ ```bash
17
+ pnpm add @gridkitjs/dom
18
+ ```
19
+
20
+ Every behaviour attaches to elements the renderer has drawn, owns its own
21
+ listeners and observers, reports through callbacks, and returns the way to end
22
+ it — so it maps onto any framework's lifecycle.
23
+
24
+ ```ts
25
+ import { createLiveAnnouncer, observeElementWidth } from "@gridkitjs/dom";
26
+
27
+ const announcer = createLiveAnnouncer(gridElement);
28
+ const disconnect = observeElementWidth(gridElement, (width) => {
29
+ controller.setOptions({ ...options, viewportWidth: width });
30
+ });
31
+
32
+ announcer.announce("Amount, column 2 of 5");
33
+
34
+ // when the grid goes away
35
+ disconnect();
36
+ announcer.destroy();
37
+ ```
38
+
39
+ | Behaviour | What it does |
40
+ | -------------------------- | ----------------------------------------------------------------------------- |
41
+ | `createLiveAnnouncer` | A polite live region for announcements no visual cue can carry. |
42
+ | `observeElementWidth` | Reports an element's width, now and on every change. |
43
+ | `attachInfiniteScroll` | Calls `onLoadMore` as a sentinel row nears the bottom of the body. |
44
+ | `startColumnResizeGesture` | Drags a column's edge into a new width. |
45
+ | `startDragReorder` | Drags a header or chip to a new place, with the drop-target hit-tests. |
46
+ | `createRowVirtualizer` | Windows rows of any height down to those near the scroll position. |
47
+ | `createGridFocus` | Keeps the grid's single tab stop and moves the browser's focus with the keys. |
48
+ | `bodyCellFrom` and others | Resolve a delegated body event to the cell, row or group header it addresses. |
49
+ | `attachOverlay` | Positions a panel beside an anchor or point, and reports when to dismiss it. |
50
+
51
+ Full documentation: [github.com/blagojablazhevski/gridkit](https://github.com/blagojablazhevski/gridkit#readme)
52
+
53
+ ## License
54
+
55
+ MIT © Blagoja Blazhevski
@@ -0,0 +1,515 @@
1
+ import { ColumnResizeSession, ColumnOrderState, GroupByState, GridFocus, NavigationModifiers, VisibleRange } from '@gridkitjs/core';
2
+
3
+ /** A live region to report changes through. */
4
+ interface LiveAnnouncer {
5
+ /**
6
+ * Has assistive technology read `text` out, politely — after whatever it is
7
+ * already saying. The same text twice in a row is read twice.
8
+ */
9
+ readonly announce: (text: string) => void;
10
+ /** Removes the live region. */
11
+ readonly destroy: () => void;
12
+ }
13
+ /**
14
+ * Creates the one live region a grid reports through — a reorder, a resize, a
15
+ * bulk selection, anything no visual cue can carry to a screen reader — as
16
+ * the last child of `container`.
17
+ *
18
+ * Polite rather than assertive, so an announcement waits for a pause instead
19
+ * of cutting across what the user is already hearing. The element carries
20
+ * `gridkit-sr-only`, which the theme clips to nothing while leaving it in the
21
+ * accessibility tree.
22
+ *
23
+ * A live region speaks only when its text changes, so a message identical to
24
+ * the last would go unheard. A no-break space, added and dropped in turn,
25
+ * makes the repeat a change without changing what is read.
26
+ */
27
+ declare function createLiveAnnouncer(container?: HTMLElement): LiveAnnouncer;
28
+
29
+ /** The data cell an event happened in. */
30
+ interface BodyCellTarget {
31
+ /** The row's place among every body row, from its `data-gridkit-row-index`. */
32
+ readonly rowIndex: number;
33
+ /** The cell's place in its row, off the table's own indices. */
34
+ readonly columnIndex: number;
35
+ readonly columnId: string;
36
+ }
37
+ /** The group-header row an event happened in. */
38
+ interface GroupRowTarget {
39
+ readonly rowIndex: number;
40
+ readonly groupId: string;
41
+ }
42
+ /** The body row an event happened in — a data row, or a group's header or summary. */
43
+ interface BodyRowTarget {
44
+ readonly rowIndex: number;
45
+ /** The column of the cell it happened in, `null` when it was in none. */
46
+ readonly columnId: string | null;
47
+ }
48
+ /**
49
+ * The data cell `target` is in — what a renderer delegating its body's
50
+ * events to one listener asks of each event. `closest` is what makes the
51
+ * whole cell the target: a click on a template's own markup resolves to the
52
+ * cell holding it.
53
+ *
54
+ * Never matches inside a group-header row: that row's single `<td>` carries
55
+ * no `data-gridkit-column` (it spans every column at once, so no one column
56
+ * id applies), which keeps this to data rows without a check of its own.
57
+ */
58
+ declare function bodyCellFrom(target: EventTarget | null): BodyCellTarget | null;
59
+ /** The group-header row (`tr[data-gridkit-group]`) `target` is in. */
60
+ declare function groupRowFrom(target: EventTarget | null): GroupRowTarget | null;
61
+ /**
62
+ * The row `target` is in, cell or not — all a row click needs. The column is
63
+ * read only off a cell of that very row, so a table inside a cell template
64
+ * cannot lend it one of its own.
65
+ */
66
+ declare function bodyRowFrom(target: EventTarget | null): BodyRowTarget | null;
67
+
68
+ /**
69
+ * The `pointerdown` a gesture starts from. A native `PointerEvent` is one,
70
+ * and so is a framework's wrapper around it, such as React's synthetic
71
+ * event, so a renderer passes whichever its handler received.
72
+ */
73
+ interface PointerPress {
74
+ readonly pointerId: number;
75
+ readonly clientX: number;
76
+ readonly clientY: number;
77
+ readonly button: number;
78
+ readonly isPrimary: boolean;
79
+ preventDefault(): void;
80
+ }
81
+
82
+ interface ColumnResizeGestureHandlers {
83
+ /** Every pointer move, with the width the column takes there. */
84
+ readonly onMove: (width: number) => void;
85
+ /**
86
+ * The pointer was released. `width` is the width to commit, or `null` when
87
+ * it ended where it started: a click on the handle — as each half of a
88
+ * double-click is — is not a resize, and reporting one would fire a
89
+ * spurious resize on every click.
90
+ */
91
+ readonly onEnd: (width: number | null) => void;
92
+ /** Escape, or a cancelled pointer: the column goes back to `session.startWidth`. */
93
+ readonly onCancel: () => void;
94
+ }
95
+ /**
96
+ * Drags a column's edge from `press` on its resize `handle`, turning pointer
97
+ * positions into widths through `@gridkitjs/core`'s `session` — begun with
98
+ * `beginColumnResize` — and reporting them. Returns whether a resize began:
99
+ * only the primary button of the primary pointer starts one.
100
+ *
101
+ * Captures the pointer on `handle`, so the drag keeps reporting wherever the
102
+ * pointer goes, and prevents the press's default, so it does not select the
103
+ * header text it started on. No minimum distance, unlike a reorder drag: the
104
+ * handle does nothing else a press could have meant.
105
+ */
106
+ declare function startColumnResizeGesture(handle: HTMLElement, press: PointerPress, session: ColumnResizeSession, handlers: ColumnResizeGestureHandlers): boolean;
107
+ /**
108
+ * The width the widest cell in `columnId` needs to show its content in full,
109
+ * or `null` when the column has no cells — what double-clicking a resize
110
+ * handle sizes the column to, through `sizeColumnToContent`.
111
+ *
112
+ * Measured with a `Range` over each cell's contents rather than the cell's own
113
+ * `scrollWidth`. A range shrink-wraps what is actually laid out, so a column
114
+ * far wider than its content reports the content width and can be sized *down*;
115
+ * `scrollWidth` never reports less than the cell it is read from and so can
116
+ * only ever grow a column.
117
+ *
118
+ * Only rendered rows are measured — on a paged or virtualized grid, the ones
119
+ * on screen.
120
+ */
121
+ declare function measureColumnContent(table: HTMLTableElement, columnId: string): number | null;
122
+
123
+ /** What a renderer draws while a reorder drag is open. */
124
+ interface DragReorderState<Target> {
125
+ /** The item being dragged, once the press has become a drag. */
126
+ readonly draggedId: string | null;
127
+ /** Where it would land if released now, `null` for nowhere valid. */
128
+ readonly dropTarget: Target | null;
129
+ /** Where the element trailing the pointer sits, as a CSS `transform`. */
130
+ readonly ghostTransform: string | null;
131
+ }
132
+ /** The state while nothing is dragged — the one a drag reports when it ends. */
133
+ declare const NO_DRAG: DragReorderState<never>;
134
+ interface DragReorderHandlers<Target> {
135
+ /**
136
+ * Where a drop at `(clientX, clientY)` would land, or `null` for nowhere
137
+ * valid — including a gap `id` already occupies, so a drop target only
138
+ * ever promises a move that happens. Called on every pointer move once the
139
+ * drag has opened. `resolveColumnDropTarget` and
140
+ * `resolveGroupByDropTarget` are the grid's own.
141
+ */
142
+ readonly resolveTarget: (clientX: number, clientY: number, id: string) => Target | null;
143
+ /** Every change to what the renderer draws, `NO_DRAG` once the drag ends. */
144
+ readonly onState: (state: DragReorderState<Target>) => void;
145
+ /** The pointer was released over a valid target. */
146
+ readonly onDrop: (target: Target, id: string) => void;
147
+ }
148
+ /**
149
+ * The pointer mechanics behind every drag-to-reorder gesture in the grid — a
150
+ * column header dragged among the others or into the group-by bar, a
151
+ * group-by chip dragged within it: capture, the drag threshold, and the ghost
152
+ * trailing the pointer, holding the spot it was picked up by. Returns whether
153
+ * a gesture began; only the main button of the primary pointer starts one.
154
+ *
155
+ * The press is left to its default, so the pressed element still takes focus;
156
+ * until the pointer has travelled `DRAG_THRESHOLD` pixels nothing is
157
+ * reported, so a press that never moves stays a click. Escape, or a cancelled
158
+ * pointer, leaves the order as it was.
159
+ *
160
+ * Pointer events rather than HTML5 drag-and-drop: `dragenter`/`dragleave`
161
+ * fire per descendant, so a dragged item's own inner markup would each report
162
+ * the target leaving and re-entering as the pointer crossed them.
163
+ */
164
+ declare function startDragReorder<Target>(id: string, grabElement: HTMLElement, press: PointerPress, handlers: DragReorderHandlers<Target>): boolean;
165
+
166
+ /** A drop into the group-by bar, in front of `beforeColumnId` — `null` to append. */
167
+ interface GroupByDropTarget {
168
+ readonly kind: "group-by";
169
+ readonly beforeColumnId: string | null;
170
+ }
171
+ /**
172
+ * Where a dragged column header would land: among the other headers, in
173
+ * front of `beforeId` (`null` for the end), or in the group-by bar.
174
+ */
175
+ type ColumnDropTarget = {
176
+ readonly kind: "column-order";
177
+ readonly beforeId: string | null;
178
+ } | GroupByDropTarget;
179
+ interface ColumnDropContext {
180
+ /** The ids of the columns on screen, in order — what a drop is resolved among. */
181
+ readonly order: ColumnOrderState;
182
+ /** What a drop onto the group-by bar is resolved against. */
183
+ readonly groupBy: GroupByState;
184
+ /** Column ids whose header may be dragged into the group-by bar at all. */
185
+ readonly groupByDraggableIds: ReadonlySet<string>;
186
+ }
187
+ /**
188
+ * Where a header or chip dropped at `(clientX, clientY)` would land within
189
+ * the group-by bar (`[data-gridkit-group-by-bar]`, its chips
190
+ * `[data-gridkit-group-chip]`), or `null` when the pointer isn't over the bar
191
+ * at all, or when the drop would change nothing. Shared by a header dragged
192
+ * in from outside the bar and a chip dragged within it, so the two can never
193
+ * disagree about where a drop lands.
194
+ */
195
+ declare function resolveGroupByDropTarget(clientX: number, clientY: number, movedId: string, groupBy: GroupByState): GroupByDropTarget | null;
196
+ /**
197
+ * Where the header of `movedId` dropped at `(clientX, clientY)` would land:
198
+ * before or after another header (`th[data-gridkit-column]`), by which half
199
+ * of it the pointer is over, or else in the group-by bar — or `null` for
200
+ * nowhere a drop would change anything.
201
+ *
202
+ * A column already grouped never targets the bar: reordering it there is its
203
+ * chip's job, not its header's.
204
+ */
205
+ declare function resolveColumnDropTarget(clientX: number, clientY: number, movedId: string, { order, groupBy, groupByDraggableIds }: ColumnDropContext): ColumnDropTarget | null;
206
+
207
+ /**
208
+ * Reports `element`'s inner width now and on every change after, until the
209
+ * returned function disconnects it — what `resizeMode: "fit"` fills.
210
+ *
211
+ * The first report is synchronous, so a renderer calling this before paint
212
+ * lays its columns out at the right width from the first frame rather than
213
+ * visibly jumping once the observer's first callback arrives.
214
+ *
215
+ * Reads `clientWidth` rather than the observer entry's `contentRect` so the
216
+ * first report and every later one come from the same, already-rounded,
217
+ * source.
218
+ */
219
+ declare function observeElementWidth(element: HTMLElement, onWidth: (width: number) => void): () => void;
220
+
221
+ /**
222
+ * The window of rows a row virtualizer has drawn, and a way to bring a row
223
+ * into it — what a focus move needs when its target row may not be mounted.
224
+ */
225
+ interface FocusVirtualRange {
226
+ readonly startIndex: number;
227
+ readonly endIndex: number;
228
+ readonly scrollToIndex: (index: number) => void;
229
+ }
230
+ /** How to find the rendered rows the browser's focus moves among. */
231
+ interface GridFocusRows {
232
+ /** The header row's element, `null` while none is rendered. */
233
+ readonly getHeaderRow: () => HTMLTableRowElement | null;
234
+ /** Body row `rowIndex`'s element — `findBodyRow` finds it — `null` while it is not rendered. */
235
+ readonly getBodyRow: (rowIndex: number) => HTMLTableRowElement | null;
236
+ }
237
+ interface GridFocusOptions {
238
+ /** Body rows, counting group headers and summaries — every row a key could step through. */
239
+ readonly rowCount: number;
240
+ readonly columnCount: number;
241
+ /**
242
+ * Rows the vertical keys step over rather than stop on — a group's own
243
+ * summary row, which takes a row slot but is not a stop of its own.
244
+ */
245
+ readonly isSkippableRow?: ((rowIndex: number) => boolean) | undefined;
246
+ /**
247
+ * The window the renderer drew, when rows are virtualized. A move can
248
+ * target a row outside it — `ArrowDown` past its end, or `Ctrl+End` — that
249
+ * no lookup would find until it is scrolled into range.
250
+ */
251
+ readonly virtualRange?: FocusVirtualRange | null | undefined;
252
+ }
253
+ interface GridFocusController {
254
+ /**
255
+ * The cell holding the grid's single tab stop, clamped into the current
256
+ * row and column counts. The same object until it changes, so it can serve
257
+ * as a renderer's snapshot.
258
+ */
259
+ readonly getFocus: () => GridFocus;
260
+ /** Calls `listener` whenever the focused cell moves. Returns the unsubscribe. */
261
+ readonly subscribe: (listener: () => void) => () => void;
262
+ /**
263
+ * Replaces the options, as a renderer does on every render with its
264
+ * current counts and window. Calls no listener: the renderer calling it is
265
+ * already drawing.
266
+ */
267
+ readonly setOptions: (options: GridFocusOptions) => void;
268
+ /**
269
+ * Moves the tab stop, and the browser's focus with it once the renderer
270
+ * has drawn the move (see `focusDrawnCell`). Moving to the cell already
271
+ * focused does nothing, which is what lets a cell's own `focus` event
272
+ * report a click through here without a loop.
273
+ */
274
+ readonly focusCell: (rowIndex: number, columnIndex: number) => void;
275
+ /**
276
+ * Moves the tab stop as `key` does — the arrows, Home, End, the page keys,
277
+ * with Ctrl for the grid's corners. Returns whether the key was one of
278
+ * them, in which case the caller prevents its default: taken even when the
279
+ * move lands nowhere, or an arrow at the last row would scroll the page
280
+ * out from under the grid.
281
+ */
282
+ readonly handleKey: (key: string, modifiers: NavigationModifiers, pageSize: number) => boolean;
283
+ /**
284
+ * Connects the tab stop to the rendered rows, which the browser's focus
285
+ * then follows it among. Returns the function that disconnects them.
286
+ */
287
+ readonly attach: (rows: GridFocusRows) => () => void;
288
+ /**
289
+ * Called by the renderer once it has drawn `drawn` — a value `getFocus`
290
+ * returned — so the browser's focus follows a move to the cell now in the
291
+ * markup. Does nothing unless a move to exactly that drawing is pending.
292
+ */
293
+ readonly focusDrawnCell: (drawn: GridFocus) => void;
294
+ }
295
+ /**
296
+ * Body row `rowIndex` in `bodyTable`, by its `data-gridkit-row-index` rather
297
+ * than by its position in `tBodies[0].rows`: a virtualized body holds only a
298
+ * slice of the rows, behind a leading spacer `<tr>`, so once the window
299
+ * scrolls a DOM position no longer matches the row's index in the full array
300
+ * and addresses some other mounted row, or none.
301
+ *
302
+ * The attribute holds a plain integer, so the selector needs no escaping,
303
+ * unlike one built from a consumer's row id.
304
+ */
305
+ declare function findBodyRow(bodyTable: HTMLTableElement | null, rowIndex: number): HTMLTableRowElement | null;
306
+ /**
307
+ * How many rows fit `bodyTable`'s scroll wrapper — its parent, the row area
308
+ * alone, not the header, footer and pager around it — which is what a page
309
+ * key moves. Reads the first rendered row's height, and falls back to ten
310
+ * rows when nothing can be measured.
311
+ */
312
+ declare function measurePageSize(bodyTable: HTMLTableElement | null): number;
313
+ /**
314
+ * The grid's single tab stop and the keys that move it, which is what
315
+ * `role="grid"` obliges: arrow keys travel cell to cell and Tab passes the
316
+ * whole grid by. The renderer attaches its rows, draws `getFocus()` —
317
+ * `tabindex="0"` on that cell, `-1` on every other — re-reads it when
318
+ * `subscribe` calls, and calls `focusDrawnCell` after drawing.
319
+ *
320
+ * Separate from selection, because focus and selection are different states:
321
+ * a cell is focused while travelling before anything is selected, and every
322
+ * cell of a range is selected without being focused. Navigation also has to
323
+ * work with selection off entirely.
324
+ *
325
+ * Starts on the header row's first cell.
326
+ */
327
+ declare function createGridFocus(initialOptions: GridFocusOptions): GridFocusController;
328
+
329
+ interface InfiniteScrollOptions {
330
+ /**
331
+ * The element whose approach triggers a load — the always-last row of the
332
+ * body — or `null` while none is rendered.
333
+ */
334
+ readonly sentinel: Element | null;
335
+ /** Whether a further load could still return more rows. `false` stops watching altogether. */
336
+ readonly hasMore: boolean;
337
+ /**
338
+ * Called at most once per approach: when the sentinel comes within
339
+ * `rootMargin` of the root's visible area, and not again until
340
+ * `isLoadingMore` has gone from `true` back to `false`.
341
+ */
342
+ readonly onLoadMore?: (() => void) | undefined;
343
+ /** Whether a load `onLoadMore` started is still in flight. */
344
+ readonly isLoadingMore?: boolean | undefined;
345
+ /** How far ahead of the sentinel scrolling into view a load starts, as CSS margins. Defaults to `"200px"`. */
346
+ readonly rootMargin?: string | undefined;
347
+ }
348
+ interface InfiniteScroll {
349
+ /**
350
+ * Takes the current options. Cheap when nothing changed, so a renderer may
351
+ * call it after every render; a new sentinel or `rootMargin` restarts the
352
+ * observer, and `isLoadingMore` going back to `false` re-checks at once.
353
+ */
354
+ readonly update: (options: InfiniteScrollOptions) => void;
355
+ /** Stops watching for good. */
356
+ readonly detach: () => void;
357
+ }
358
+ /**
359
+ * Turns a sentinel's approach to the bottom of the scrolling `root` into
360
+ * `onLoadMore` calls, no more than one per "the sentinel is near and no load
361
+ * is in flight since the last one finished".
362
+ *
363
+ * Three facts drive it: whether the sentinel is intersecting (as of the
364
+ * observer's last report), whether `onLoadMore` has fired since the last
365
+ * load finished, and whether a load was in flight at the last update. They
366
+ * outlive the sentinel itself, which a renderer mounts and unmounts as
367
+ * `hasMore` changes, so they live here rather than on any one observer.
368
+ *
369
+ * Off means off: with `hasMore` false, or no sentinel, no observer exists.
370
+ */
371
+ declare function attachInfiniteScroll(root: Element, options: InfiniteScrollOptions): InfiniteScroll;
372
+
373
+ /** Which side of its anchor an overlay opens on. */
374
+ type OverlaySide = "top" | "bottom" | "left" | "right";
375
+ /**
376
+ * Which edge of the anchor the overlay lines up with along that side: its
377
+ * left edge (`start`) or right edge (`end`) for `top`/`bottom`, its top or
378
+ * bottom edge for `left`/`right`.
379
+ */
380
+ type OverlayAlign = "start" | "end";
381
+ type OverlayPlacement = `${OverlaySide}-${OverlayAlign}`;
382
+ /** A box in viewport coordinates, as `getBoundingClientRect` reports one. */
383
+ interface OverlayRect {
384
+ readonly top: number;
385
+ readonly left: number;
386
+ readonly width: number;
387
+ readonly height: number;
388
+ }
389
+ interface OverlaySize {
390
+ readonly width: number;
391
+ readonly height: number;
392
+ }
393
+ interface OverlayPlacementOptions {
394
+ /** Where the overlay would rather open. Defaults to `"bottom-start"`. */
395
+ readonly placement?: OverlayPlacement | undefined;
396
+ /** Space between the anchor and the overlay, in pixels. Defaults to `0`. */
397
+ readonly gap?: number | undefined;
398
+ }
399
+ /** Where an overlay goes: its top-left corner, and the placement it ended up with. */
400
+ interface OverlayPosition {
401
+ readonly top: number;
402
+ readonly left: number;
403
+ readonly placement: OverlayPlacement;
404
+ }
405
+ /**
406
+ * Where an overlay of size `panel` goes beside `anchor` so that it stays in
407
+ * `viewport`: on the preferred side unless only the opposite one has room,
408
+ * lined up with the preferred edge unless that runs it off screen and the
409
+ * other edge does not, and finally pushed back inside if it still overflows.
410
+ * Pure, so the rules are testable without a browser; `attachOverlay` feeds it
411
+ * the measurements.
412
+ *
413
+ * A point — a context menu opened where the pointer is — is an anchor with
414
+ * no width or height, which is what makes `start`/`end` mean "after" and
415
+ * "before" it.
416
+ */
417
+ declare function placeOverlay(anchor: OverlayRect, panel: OverlaySize, viewport: OverlaySize, options?: OverlayPlacementOptions): OverlayPosition;
418
+
419
+ /** What an overlay opens beside: an element, or a point in viewport coordinates. */
420
+ type OverlayAnchor = Element | {
421
+ readonly x: number;
422
+ readonly y: number;
423
+ };
424
+ /** Why an overlay asked to close. */
425
+ type OverlayDismissReason = "escape" | "outside" | "scroll" | "resize" | "blur";
426
+ interface OverlayOptions extends OverlayPlacementOptions {
427
+ /**
428
+ * The overlay asks to close: Escape was pressed, a pointer went down
429
+ * outside it, something outside it scrolled, the window resized or lost
430
+ * focus, or focus moved outside it. The caller closes it — removes the
431
+ * panel and detaches — or keeps it open by doing nothing.
432
+ */
433
+ readonly onDismiss: (reason: OverlayDismissReason) => void;
434
+ /**
435
+ * Where focus goes when the overlay detaches. Defaults to the element that
436
+ * had focus when it attached; `null` leaves focus alone.
437
+ */
438
+ readonly returnFocusTo?: HTMLElement | null | undefined;
439
+ }
440
+ /**
441
+ * Shows `panel` as an overlay beside `anchor` — a menu, a popover, a filter
442
+ * editor — and returns the function that ends it. The caller renders the
443
+ * panel, usually as a child of `document.body` so no scrolling ancestor clips
444
+ * it; this positions it, moves focus into it, and reports through
445
+ * `onDismiss` when the user is done with it.
446
+ *
447
+ * The panel is positioned `fixed`, at the place `placeOverlay` picks from the
448
+ * anchor's and the panel's measured boxes, and carries that placement in
449
+ * `data-gridkit-placement` (`"top-start"` once flipped above, say) for a
450
+ * stylesheet to point an arrow by. Focus moves to the panel's first focusable
451
+ * descendant, or to the panel itself — given `tabindex="-1"` if it has no
452
+ * `tabindex` — when there is none.
453
+ *
454
+ * Nothing that happens on or in the anchor dismisses the overlay, so a button
455
+ * that toggles it can close it with its own click rather than having the
456
+ * overlay close and the click reopen it.
457
+ *
458
+ * Detaching removes every listener and returns focus, but only when focus is
459
+ * still in the panel or nowhere — a click that dismissed it by landing on
460
+ * another control leaves focus on that control.
461
+ */
462
+ declare function attachOverlay(anchor: OverlayAnchor, panel: HTMLElement, options: OverlayOptions): () => void;
463
+
464
+ interface RowVirtualizerOptions {
465
+ /** The height assumed for a row not yet measured. */
466
+ readonly estimatedRowHeight: number;
467
+ /** Rows rendered beyond each edge of the visible area. */
468
+ readonly overscan: number;
469
+ /**
470
+ * The window of rows to render moved: called whenever a scroll, a resize
471
+ * of the scroll element, a measured row height or a change of rows moves
472
+ * it, and never with a window equal to the last.
473
+ */
474
+ readonly onRange: (range: VisibleRange) => void;
475
+ }
476
+ interface RowVirtualizer {
477
+ /** How many rows there are. Measured heights of the rows that remain are kept. */
478
+ readonly setRowCount: (count: number) => void;
479
+ /** Forgets every measured height, for rows that are no longer the ones measured. */
480
+ readonly resetHeights: () => void;
481
+ /**
482
+ * A ref callback for the element of row `index` (its place among all the
483
+ * rows, not among those rendered): measured while mounted, forgotten once
484
+ * called with `null`. The same function for the same index every time, so
485
+ * a renderer re-attaching it on every render costs nothing.
486
+ */
487
+ readonly measureRow: (index: number) => (element: HTMLElement | null) => void;
488
+ /**
489
+ * Scrolls so row `index` is in view. A mounted row is scrolled to directly;
490
+ * one outside the window is jumped to at its estimated offset, then
491
+ * scrolled to properly once it mounts and can be measured.
492
+ */
493
+ readonly scrollToIndex: (index: number, options?: ScrollIntoViewOptions) => void;
494
+ /** Stops listening and measuring, for good. */
495
+ readonly destroy: () => void;
496
+ }
497
+ /**
498
+ * Windows a long list of rows inside `scrollElement` down to those near its
499
+ * scroll position, measuring each rendered row's real height rather than
500
+ * assuming a uniform one — rows are not uniform (`.is-wrapped` cells, group
501
+ * header and summary rows), so a fixed-height virtualizer would either break
502
+ * wrapping or rule it out. The renderer draws `onRange`'s window, with
503
+ * spacers of `offsetBefore`/`offsetAfter` around it, and hands each row's
504
+ * element to `measureRow`.
505
+ *
506
+ * The heights are a mutable array, not a structure rebuilt per measurement:
507
+ * one can arrive many times a second while scrolling. The pure arithmetic
508
+ * over them is `@gridkitjs/core`'s `visibleRange`; this owns only the
509
+ * mutability around it and the listeners that feed it — the scroll position,
510
+ * the scroll element's height, and one `ResizeObserver` shared by every
511
+ * mounted row.
512
+ */
513
+ declare function createRowVirtualizer(scrollElement: HTMLElement, options: RowVirtualizerOptions): RowVirtualizer;
514
+
515
+ export { type BodyCellTarget, type BodyRowTarget, type ColumnDropContext, type ColumnDropTarget, type ColumnResizeGestureHandlers, type DragReorderHandlers, type DragReorderState, type FocusVirtualRange, type GridFocusController, type GridFocusOptions, type GridFocusRows, type GroupByDropTarget, type GroupRowTarget, type InfiniteScroll, type InfiniteScrollOptions, type LiveAnnouncer, NO_DRAG, type OverlayAlign, type OverlayAnchor, type OverlayDismissReason, type OverlayOptions, type OverlayPlacement, type OverlayPlacementOptions, type OverlayPosition, type OverlayRect, type OverlaySide, type OverlaySize, type PointerPress, type RowVirtualizer, type RowVirtualizerOptions, attachInfiniteScroll, attachOverlay, bodyCellFrom, bodyRowFrom, createGridFocus, createLiveAnnouncer, createRowVirtualizer, findBodyRow, groupRowFrom, measureColumnContent, measurePageSize, observeElementWidth, placeOverlay, resolveColumnDropTarget, resolveGroupByDropTarget, startColumnResizeGesture, startDragReorder };