@fusion.dev/helix-headless 0.6.0 → 0.7.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.
@@ -5,13 +5,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessDataTable_1;
8
- import { customElement, HelixElement, HelixFilterEvent, HelixNavigateEvent, HelixSortEvent, hostAria, MixinCollection, requestChange, slotFilled, } from '@fusion.dev/helix-core';
8
+ import { customElement, HelixElement, HelixFilterEvent, HelixNavigateEvent, HelixSortEvent, hostAria, MixinCollection, requestChange, slotFilled, VirtualizeController, } from '@fusion.dev/helix-core';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { repeat } from 'lit/directives/repeat.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import '../../pagination/index.js';
14
- import { cycle, format, ordered, parse } from './order.js';
14
+ import { arranged, cycle, format, ordered, parse, window_, } from './order.js';
15
15
  let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollection(HelixElement) {
16
16
  constructor() {
17
17
  super(...arguments);
@@ -19,20 +19,35 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
19
19
  this.defaultPage = 1;
20
20
  this.defaultPageSize = 0;
21
21
  this.defaultSort = '';
22
+ this.estimate = 32;
22
23
  this.key = '';
23
24
  this.label = '';
24
25
  this.mode = 'client';
25
26
  this.rows = [];
26
27
  this.total = 0;
28
+ this.windowed = false;
27
29
  this.sortState = undefined;
28
30
  this.filterState = undefined;
29
31
  this.pageState = undefined;
30
32
  this.pageSizeState = undefined;
31
33
  this.paginatedState = undefined;
34
+ this.virtualize = new VirtualizeController(this, {
35
+ container: () => this.windowed ? this.renderRoot.querySelector('[part="table"]') : null,
36
+ count: () => this.available,
37
+ estimate: () => this.estimate,
38
+ measured: () => this.windowed,
39
+ rows: () => (this.windowed ? this.drawn() : []),
40
+ });
32
41
  this.warnedAboutSize = false;
33
42
  this.handleSlotChange = () => {
34
43
  this.requestUpdate();
35
44
  };
45
+ this.measuredAgainst = '';
46
+ this.handleScroll = () => {
47
+ if (this.windowed) {
48
+ this.virtualize.measure();
49
+ }
50
+ };
36
51
  this.handleHeadClick = (event) => {
37
52
  const column = this.columnFrom(event);
38
53
  if (column?.canSort) {
@@ -103,6 +118,37 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
103
118
  this.paginatedState = next;
104
119
  this.requestUpdate('paginated', previous);
105
120
  }
121
+ drawn() {
122
+ return [...this.renderRoot.querySelectorAll('[part="row"][data-index]')].map((element) => ({ element, index: Number(element.dataset.index) }));
123
+ }
124
+ get available() {
125
+ if (this.mode === 'server') {
126
+ return this.rows.length;
127
+ }
128
+ const matched = this.windowed ? this.arrange().rows.length : this.matched;
129
+ const size = this.pageSize;
130
+ if (size <= 0) {
131
+ return matched;
132
+ }
133
+ return Math.max(0, Math.min(size, matched - (this.page - 1) * size));
134
+ }
135
+ arrange() {
136
+ const axes = this.axes;
137
+ const key = `${this.filter}\u0000${this.sort}\u0000${axes.map((axis) => axis.field).join('\u0001')}`;
138
+ if (this.held?.key === key && this.held.source === this.rows) {
139
+ return this.held.value;
140
+ }
141
+ const value = arranged(this.rows, axes, { query: this.filter, spec: this.sort });
142
+ this.held = { key, source: this.rows, value };
143
+ return value;
144
+ }
145
+ windowOf(rows) {
146
+ if (!this.windowed) {
147
+ return rows;
148
+ }
149
+ const { first, last } = this.virtualize.window;
150
+ return rows.slice(first, last);
151
+ }
106
152
  get tableColumns() {
107
153
  return [...(this.children ?? [])].filter((child) => 'isTableColumn' in child && child.isTableColumn === true);
108
154
  }
@@ -131,16 +177,26 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
131
177
  get view() {
132
178
  if (this.mode === 'server') {
133
179
  const total = Math.max(this.total, this.rows.length);
134
- return { matched: total, rows: [...this.rows], total };
180
+ return {
181
+ available: this.rows.length,
182
+ matched: total,
183
+ rows: this.windowOf([...this.rows]),
184
+ total,
185
+ };
135
186
  }
136
- const view = ordered(this.rows, this.axes, {
137
- page: this.page,
138
- query: this.filter,
139
- size: this.pageSize,
140
- spec: this.sort,
141
- });
142
- this.warnIfCrowded(view.rows.length);
143
- return view;
187
+ if (!this.windowed) {
188
+ const view = ordered(this.rows, this.axes, {
189
+ page: this.page,
190
+ query: this.filter,
191
+ size: this.pageSize,
192
+ spec: this.sort,
193
+ });
194
+ this.warnIfCrowded(view.rows.length);
195
+ return { ...view, available: view.rows.length };
196
+ }
197
+ const { rows: inOrder, total } = this.arrange();
198
+ const page = window_(inOrder, this.page, this.pageSize);
199
+ return { available: page.length, matched: inOrder.length, rows: this.windowOf(page), total };
144
200
  }
145
201
  get pages() {
146
202
  const size = this.pageSize;
@@ -237,6 +293,11 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
237
293
  willUpdate(changed) {
238
294
  super.willUpdate(changed);
239
295
  hostAria(this.internals, { label: this.label || 'Table', role: 'grid' });
296
+ const placement = this.placement;
297
+ if (changed.has('rows') || placement !== this.measuredAgainst) {
298
+ this.measuredAgainst = placement;
299
+ this.virtualize.forget();
300
+ }
240
301
  }
241
302
  markEmpty() {
242
303
  this.markRegion('toolbar', ['toolbar']);
@@ -253,6 +314,9 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
253
314
  this.markEmpty();
254
315
  this.assign();
255
316
  this.announceSort();
317
+ if (this.windowed) {
318
+ this.virtualize.measure();
319
+ }
256
320
  }
257
321
  announceSort() {
258
322
  const current = parse(this.sort);
@@ -276,21 +340,38 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
276
340
  renderRows(rows) {
277
341
  const columns = this.tableColumns;
278
342
  const field = this.key;
279
- return html `${repeat(rows, (row, index) => (field ? String(row[field] ?? index) : index), (row, index) => html `<div
343
+ const start = this.windowStart;
344
+ return html `${repeat(rows, (row, index) => (field ? String(row[field] ?? start + index) : start + index), (row, index) => html `<div
280
345
  part="row"
281
346
  role="row"
282
347
  aria-rowindex=${this.rowIndex(index)}
348
+ data-index=${this.windowed ? start + index : nothing}
283
349
  >
284
350
  ${columns.map((column) => this.renderCell(column, row))}
285
351
  </div>`)}`;
286
352
  }
287
353
  rowIndex(within) {
288
354
  const size = this.pageSize;
289
- const offset = size > 0 ? (this.page - 1) * size : 0;
290
- return offset + within + 2;
355
+ const page = size > 0 ? (this.page - 1) * size : 0;
356
+ return page + this.windowStart + within + 2;
357
+ }
358
+ get placement() {
359
+ return [this.mode, this.filter, this.sort, this.page, this.pageSize].join('\u0000');
360
+ }
361
+ get windowStart() {
362
+ return this.windowed ? this.virtualize.window.first : 0;
363
+ }
364
+ renderSpacer(end) {
365
+ if (!this.windowed) {
366
+ return nothing;
367
+ }
368
+ const { after, before } = this.virtualize.window;
369
+ return end === 'before'
370
+ ? html `<div part="before" role="presentation" style="block-size: ${before}px"></div>`
371
+ : html `<div part="after" role="presentation" style="block-size: ${after}px"></div>`;
291
372
  }
292
373
  render() {
293
- const { matched, rows } = this.view;
374
+ const { available, matched, rows } = this.view;
294
375
  const template = this.template;
295
376
  const columns = this.tableColumns;
296
377
  const pagination = this.tag('pagination');
@@ -299,14 +380,16 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
299
380
  part="table"
300
381
  role="rowgroup"
301
382
  aria-busy=${this.loading ? 'true' : nothing}
302
- aria-rowcount=${this.mode === 'server' ? Math.max(this.total, rows.length) + 1 : matched + 1}
383
+ aria-rowcount=${this.mode === 'server' ? Math.max(this.total, available) + 1 : matched + 1}
303
384
  aria-colcount=${columns.length || nothing}
304
385
  style=${template ? `--data-table-template: ${template}` : nothing}
386
+ @scroll=${this.handleScroll}
305
387
  >
306
388
  <div part="head" role="row">
307
389
  <slot name="columns" @slotchange=${this.handleSlotChange}></slot>
308
390
  </div>
309
- ${this.renderRows(rows)} ${this.renderStatus(rows.length > 0)}
391
+ ${this.renderSpacer('before')}${this.renderRows(rows)}${this.renderSpacer('after')}
392
+ ${this.renderStatus(available > 0)}
310
393
  </div>
311
394
  <div part="footer">
312
395
  <slot name="footer"></slot>
@@ -354,23 +437,26 @@ __decorate([
354
437
  property({ attribute: 'default-sort' })
355
438
  ], HelixHeadlessDataTable.prototype, "defaultSort", void 0);
356
439
  __decorate([
357
- property()
358
- ], HelixHeadlessDataTable.prototype, "key", void 0);
440
+ property({ type: Number })
441
+ ], HelixHeadlessDataTable.prototype, "estimate", void 0);
359
442
  __decorate([
360
443
  property()
361
444
  ], HelixHeadlessDataTable.prototype, "sort", null);
362
445
  __decorate([
363
446
  property()
364
- ], HelixHeadlessDataTable.prototype, "label", void 0);
447
+ ], HelixHeadlessDataTable.prototype, "key", void 0);
365
448
  __decorate([
366
449
  property()
367
450
  ], HelixHeadlessDataTable.prototype, "filter", null);
368
451
  __decorate([
369
452
  property()
370
- ], HelixHeadlessDataTable.prototype, "mode", void 0);
453
+ ], HelixHeadlessDataTable.prototype, "label", void 0);
371
454
  __decorate([
372
455
  property({ type: Number })
373
456
  ], HelixHeadlessDataTable.prototype, "page", null);
457
+ __decorate([
458
+ property()
459
+ ], HelixHeadlessDataTable.prototype, "mode", void 0);
374
460
  __decorate([
375
461
  property({ attribute: false })
376
462
  ], HelixHeadlessDataTable.prototype, "rows", void 0);
@@ -380,6 +466,9 @@ __decorate([
380
466
  __decorate([
381
467
  property({ type: Number })
382
468
  ], HelixHeadlessDataTable.prototype, "total", void 0);
469
+ __decorate([
470
+ property({ reflect: true, type: Boolean })
471
+ ], HelixHeadlessDataTable.prototype, "windowed", void 0);
383
472
  __decorate([
384
473
  property({ type: Boolean })
385
474
  ], HelixHeadlessDataTable.prototype, "paginated", null);
@@ -92,6 +92,31 @@ export declare const sort: (rows: readonly DataRow[], spec: Sort | undefined, ax
92
92
  * same clamp `helix-pagination` already applies to what it reports.
93
93
  */
94
94
  export declare const window_: (rows: readonly DataRow[], page: number, size: number) => DataRow[];
95
+ /** What arranging produces: the rows in order, with no page or window taken. */
96
+ export interface Arranged {
97
+ /** Rows after filtering and sorting, in the order they are to be drawn. */
98
+ rows: DataRow[];
99
+ /** Rows before filtering, which is what tells a reader a filter is on. */
100
+ total: number;
101
+ }
102
+ /**
103
+ * Filtering and sorting, with nothing taken off the end.
104
+ *
105
+ * **Split out of `ordered` because a window moves and this does not.** A table
106
+ * drawing a window on a result re-renders every time a scroll reveals a row,
107
+ * and deriving that render from the whole result means filtering and sorting it
108
+ * again each time: 23.7ms at five thousand rows, a frame budget spent three
109
+ * times over before a cell is drawn. A caller that keeps this between renders
110
+ * pays it once per change to the data rather than once per frame.
111
+ *
112
+ * The two steps stay together for the reason `ordered` gives below: sorting a
113
+ * page is the classic bug, and one function doing both is what makes calling
114
+ * them in the wrong order impossible.
115
+ */
116
+ export declare const arranged: (rows: readonly DataRow[], axes: readonly Axis[], { query, spec }: {
117
+ query: string;
118
+ spec: string;
119
+ }) => Arranged;
95
120
  /** What one pass over the data produces: the window, and the counts around it. */
96
121
  export interface Ordered {
97
122
  /** Rows after filtering, before paging. The number a pagination must page. */
@@ -66,8 +66,11 @@ const compare = (left, right, sign) => {
66
66
  }
67
67
  return (sign * text(left).localeCompare(text(right), undefined, { numeric: true, sensitivity: 'base' }));
68
68
  };
69
+ export const arranged = (rows, axes, { query, spec }) => ({
70
+ rows: sort(filter(rows, query, axes), parse(spec), axes),
71
+ total: rows.length,
72
+ });
69
73
  export const ordered = (rows, axes, { page, query, size, spec }) => {
70
- const matched = filter(rows, query, axes);
71
- const arranged = sort(matched, parse(spec), axes);
72
- return { matched: matched.length, rows: window_(arranged, page, size), total: rows.length };
74
+ const { rows: inOrder, total } = arranged(rows, axes, { query, spec });
75
+ return { matched: inOrder.length, rows: window_(inOrder, page, size), total };
73
76
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-headless",
3
3
  "description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "bugs": "https://github.com/project-fusion/fusion/issues",
47
47
  "dependencies": {
48
- "@fusion.dev/helix-core": "0.6.0",
49
- "@fusion.dev/helix-icons": "0.6.0",
48
+ "@fusion.dev/helix-core": "0.7.0",
49
+ "@fusion.dev/helix-icons": "0.7.0",
50
50
  "uqr": "^0.1.3"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@custom-elements-manifest/analyzer": "^0.11.0",
54
- "@fusion.dev/helix-cem": "0.6.0",
55
- "@fusion.dev/helix-testing": "0.6.0",
54
+ "@fusion.dev/helix-cem": "0.7.0",
55
+ "@fusion.dev/helix-testing": "0.7.0",
56
56
  "@fusion.dev/scripts": "0.0.0-dev",
57
57
  "@happy-dom/global-registrator": "^20.0.0",
58
58
  "@lit-labs/ssr": "^3.3.0",
package/types/jsx.d.ts CHANGED
@@ -3838,6 +3838,20 @@ export type HelixHeadlessDataTableProps = {
3838
3838
  "default-sort"?: (HelixHeadlessDataTable["defaultSort"]) | undefined;
3839
3839
  /** Order to open on, where nothing declared one. */
3840
3840
  defaultSort?: (HelixHeadlessDataTable["defaultSort"]) | undefined;
3841
+ /** How tall a row is taken to be before it has been drawn, in pixels.
3842
+
3843
+ Only read while `windowed`, and a starting guess rather than a claim: a
3844
+ cell that wraps makes its row taller than its neighbors, and the window
3845
+ replaces this with what each row came to as it draws. It still has to be
3846
+ close, because it is what the scrollbar is built from before anything has
3847
+ been measured. */
3848
+ estimate?: (HelixHeadlessDataTable["estimate"]) | undefined;
3849
+ /** The order, as `field:direction`.
3850
+
3851
+ Set it and this element defers: a reader's click fires `helix-sort` and
3852
+ changes nothing until a new value arrives. `default-sort` seeds an order this
3853
+ element then owns. */
3854
+ sort?: (HelixHeadlessDataTable["sort"]) | undefined;
3841
3855
  /** The field naming a row's stable identity.
3842
3856
 
3843
3857
  **Needed before selection exists, not after.** Lit keys the rendered rows by
@@ -3847,20 +3861,20 @@ attached to the row it belongs to. Left unset the index is the key, which is
3847
3861
  correct only while nothing reorders: it is why this defaults to something
3848
3862
  that works and is worth setting anyway. */
3849
3863
  key?: (HelixHeadlessDataTable["key"]) | undefined;
3850
- /** The order, as `field:direction`.
3851
-
3852
- Set it and this element defers: a reader's click fires `helix-sort` and
3853
- changes nothing until a new value arrives. `default-sort` seeds an order this
3854
- element then owns. */
3855
- sort?: (HelixHeadlessDataTable["sort"]) | undefined;
3856
- /** Accessible name for the grid. */
3857
- label?: (HelixHeadlessDataTable["label"]) | undefined;
3858
3864
  /** What the rows are narrowed to, as one query across every column.
3859
3865
 
3860
3866
  One query rather than a predicate per column, which is what a reader means
3861
3867
  by a search box above a table. It matches on each column's ordering value,
3862
3868
  so search and sort cannot disagree about what a column holds. */
3863
3869
  filter?: (HelixHeadlessDataTable["filter"]) | undefined;
3870
+ /** Accessible name for the grid. */
3871
+ label?: (HelixHeadlessDataTable["label"]) | undefined;
3872
+ /** The page on show, from 1.
3873
+
3874
+ Clamped on the way out, so what this reports is the page it drew: a
3875
+ bookmarked link to page 99 of a result that has since shrunk reads back as
3876
+ the last page rather than as 99. */
3877
+ page?: (HelixHeadlessDataTable["page"]) | undefined;
3864
3878
  /** Whether this element does the work or asks the consumer to.
3865
3879
 
3866
3880
  `client` filters, sorts and pages `rows` itself. `server` applies none of
@@ -3870,12 +3884,6 @@ the client cannot hold the data, so this is not an optimization, it is the
3870
3884
  only mode that works, and it is here from the start because adding it later
3871
3885
  inverts the data flow. */
3872
3886
  mode?: (HelixHeadlessDataTable["mode"]) | undefined;
3873
- /** The page on show, from 1.
3874
-
3875
- Clamped on the way out, so what this reports is the page it drew: a
3876
- bookmarked link to page 99 of a result that has since shrunk reads back as
3877
- the last page rather than as 99. */
3878
- page?: (HelixHeadlessDataTable["page"]) | undefined;
3879
3887
  /** How many rows a page holds. Zero is every row, which is what "no paging" means.
3880
3888
 
3881
3889
  **It announces nothing, and that is not an omission.** Nothing in this
@@ -3899,6 +3907,24 @@ nothing about the result. Ignored in `client` mode rather than trusted,
3899
3907
  because two sources for one count is the mistake, and here the array is the
3900
3908
  one that cannot be stale. */
3901
3909
  total?: (HelixHeadlessDataTable["total"]) | undefined;
3910
+ /** Draws the rows in view and stands empty space in for the rest.
3911
+
3912
+ **Not a `mode`.** `mode` says where the rows come from and who filtered
3913
+ them; this says how many of the ones we hold get drawn. They are
3914
+ independent: a consumer fetching five thousand rows a chunk at a time wants
3915
+ `mode="server"` and this at once, and folding this into `mode` would be an
3916
+ API that forbids that combination for no reason.
3917
+
3918
+ **Composes with `paginated` rather than excluding it.** A window over a
3919
+ page is coherent -- the page is the slice of the result, the window is the
3920
+ part of that slice on screen -- and the arithmetic is the same either way,
3921
+ so there is nothing to forbid. Together they suit a table paging at a
3922
+ thousand rows, where the page is a reasonable fetch and a poor render.
3923
+
3924
+ Off by default. Windowing costs a scroll listener, a measurement per row
3925
+ drawn, and a `block-size` on the scrolling box, and a table of twenty rows
3926
+ should pay none of them. */
3927
+ windowed?: (HelixHeadlessDataTable["windowed"]) | undefined;
3902
3928
  /** Whether to draw a pagination under the grid.
3903
3929
 
3904
3930
  On by default once a page size is set, because a paged table with no way to
@@ -3958,6 +3984,20 @@ export type HelixHeadlessDataTableSolidJsProps = {
3958
3984
  "attr:default-sort"?: (HelixHeadlessDataTable["defaultSort"]) | undefined;
3959
3985
  /** Order to open on, where nothing declared one. */
3960
3986
  "prop:defaultSort"?: (HelixHeadlessDataTable["defaultSort"]) | undefined;
3987
+ /** How tall a row is taken to be before it has been drawn, in pixels.
3988
+
3989
+ Only read while `windowed`, and a starting guess rather than a claim: a
3990
+ cell that wraps makes its row taller than its neighbors, and the window
3991
+ replaces this with what each row came to as it draws. It still has to be
3992
+ close, because it is what the scrollbar is built from before anything has
3993
+ been measured. */
3994
+ "prop:estimate"?: (HelixHeadlessDataTable["estimate"]) | undefined;
3995
+ /** The order, as `field:direction`.
3996
+
3997
+ Set it and this element defers: a reader's click fires `helix-sort` and
3998
+ changes nothing until a new value arrives. `default-sort` seeds an order this
3999
+ element then owns. */
4000
+ "prop:sort"?: (HelixHeadlessDataTable["sort"]) | undefined;
3961
4001
  /** The field naming a row's stable identity.
3962
4002
 
3963
4003
  **Needed before selection exists, not after.** Lit keys the rendered rows by
@@ -3967,20 +4007,20 @@ attached to the row it belongs to. Left unset the index is the key, which is
3967
4007
  correct only while nothing reorders: it is why this defaults to something
3968
4008
  that works and is worth setting anyway. */
3969
4009
  "prop:key"?: (HelixHeadlessDataTable["key"]) | undefined;
3970
- /** The order, as `field:direction`.
3971
-
3972
- Set it and this element defers: a reader's click fires `helix-sort` and
3973
- changes nothing until a new value arrives. `default-sort` seeds an order this
3974
- element then owns. */
3975
- "prop:sort"?: (HelixHeadlessDataTable["sort"]) | undefined;
3976
- /** Accessible name for the grid. */
3977
- "prop:label"?: (HelixHeadlessDataTable["label"]) | undefined;
3978
4010
  /** What the rows are narrowed to, as one query across every column.
3979
4011
 
3980
4012
  One query rather than a predicate per column, which is what a reader means
3981
4013
  by a search box above a table. It matches on each column's ordering value,
3982
4014
  so search and sort cannot disagree about what a column holds. */
3983
4015
  "prop:filter"?: (HelixHeadlessDataTable["filter"]) | undefined;
4016
+ /** Accessible name for the grid. */
4017
+ "prop:label"?: (HelixHeadlessDataTable["label"]) | undefined;
4018
+ /** The page on show, from 1.
4019
+
4020
+ Clamped on the way out, so what this reports is the page it drew: a
4021
+ bookmarked link to page 99 of a result that has since shrunk reads back as
4022
+ the last page rather than as 99. */
4023
+ "prop:page"?: (HelixHeadlessDataTable["page"]) | undefined;
3984
4024
  /** Whether this element does the work or asks the consumer to.
3985
4025
 
3986
4026
  `client` filters, sorts and pages `rows` itself. `server` applies none of
@@ -3990,12 +4030,6 @@ the client cannot hold the data, so this is not an optimization, it is the
3990
4030
  only mode that works, and it is here from the start because adding it later
3991
4031
  inverts the data flow. */
3992
4032
  "prop:mode"?: (HelixHeadlessDataTable["mode"]) | undefined;
3993
- /** The page on show, from 1.
3994
-
3995
- Clamped on the way out, so what this reports is the page it drew: a
3996
- bookmarked link to page 99 of a result that has since shrunk reads back as
3997
- the last page rather than as 99. */
3998
- "prop:page"?: (HelixHeadlessDataTable["page"]) | undefined;
3999
4033
  /** How many rows a page holds. Zero is every row, which is what "no paging" means.
4000
4034
 
4001
4035
  **It announces nothing, and that is not an omission.** Nothing in this
@@ -4019,6 +4053,24 @@ nothing about the result. Ignored in `client` mode rather than trusted,
4019
4053
  because two sources for one count is the mistake, and here the array is the
4020
4054
  one that cannot be stale. */
4021
4055
  "prop:total"?: (HelixHeadlessDataTable["total"]) | undefined;
4056
+ /** Draws the rows in view and stands empty space in for the rest.
4057
+
4058
+ **Not a `mode`.** `mode` says where the rows come from and who filtered
4059
+ them; this says how many of the ones we hold get drawn. They are
4060
+ independent: a consumer fetching five thousand rows a chunk at a time wants
4061
+ `mode="server"` and this at once, and folding this into `mode` would be an
4062
+ API that forbids that combination for no reason.
4063
+
4064
+ **Composes with `paginated` rather than excluding it.** A window over a
4065
+ page is coherent -- the page is the slice of the result, the window is the
4066
+ part of that slice on screen -- and the arithmetic is the same either way,
4067
+ so there is nothing to forbid. Together they suit a table paging at a
4068
+ thousand rows, where the page is a reasonable fetch and a poor render.
4069
+
4070
+ Off by default. Windowing costs a scroll listener, a measurement per row
4071
+ drawn, and a `block-size` on the scrolling box, and a table of twenty rows
4072
+ should pay none of them. */
4073
+ "prop:windowed"?: (HelixHeadlessDataTable["windowed"]) | undefined;
4022
4074
  /** Whether to draw a pagination under the grid.
4023
4075
 
4024
4076
  On by default once a page size is set, because a paged table with no way to
@@ -18758,6 +18810,18 @@ export type CustomElements = {
18758
18810
  * - `default-page`/`defaultPage`: Page to open on, where nothing declared one.
18759
18811
  * - `default-page-size`/`defaultPageSize`: Page size to open with, where nothing declared one.
18760
18812
  * - `default-sort`/`defaultSort`: Order to open on, where nothing declared one.
18813
+ * - `estimate`: How tall a row is taken to be before it has been drawn, in pixels.
18814
+ *
18815
+ * Only read while `windowed`, and a starting guess rather than a claim: a
18816
+ * cell that wraps makes its row taller than its neighbors, and the window
18817
+ * replaces this with what each row came to as it draws. It still has to be
18818
+ * close, because it is what the scrollbar is built from before anything has
18819
+ * been measured.
18820
+ * - `sort`: The order, as `field:direction`.
18821
+ *
18822
+ * Set it and this element defers: a reader's click fires `helix-sort` and
18823
+ * changes nothing until a new value arrives. `default-sort` seeds an order this
18824
+ * element then owns.
18761
18825
  * - `key`: The field naming a row's stable identity.
18762
18826
  *
18763
18827
  * **Needed before selection exists, not after.** Lit keys the rendered rows by
@@ -18766,17 +18830,17 @@ export type CustomElements = {
18766
18830
  * attached to the row it belongs to. Left unset the index is the key, which is
18767
18831
  * correct only while nothing reorders: it is why this defaults to something
18768
18832
  * that works and is worth setting anyway.
18769
- * - `sort`: The order, as `field:direction`.
18770
- *
18771
- * Set it and this element defers: a reader's click fires `helix-sort` and
18772
- * changes nothing until a new value arrives. `default-sort` seeds an order this
18773
- * element then owns.
18774
- * - `label`: Accessible name for the grid.
18775
18833
  * - `filter`: What the rows are narrowed to, as one query across every column.
18776
18834
  *
18777
18835
  * One query rather than a predicate per column, which is what a reader means
18778
18836
  * by a search box above a table. It matches on each column's ordering value,
18779
18837
  * so search and sort cannot disagree about what a column holds.
18838
+ * - `label`: Accessible name for the grid.
18839
+ * - `page`: The page on show, from 1.
18840
+ *
18841
+ * Clamped on the way out, so what this reports is the page it drew: a
18842
+ * bookmarked link to page 99 of a result that has since shrunk reads back as
18843
+ * the last page rather than as 99.
18780
18844
  * - `mode`: Whether this element does the work or asks the consumer to.
18781
18845
  *
18782
18846
  * `client` filters, sorts and pages `rows` itself. `server` applies none of
@@ -18785,11 +18849,6 @@ export type CustomElements = {
18785
18849
  * the client cannot hold the data, so this is not an optimization, it is the
18786
18850
  * only mode that works, and it is here from the start because adding it later
18787
18851
  * inverts the data flow.
18788
- * - `page`: The page on show, from 1.
18789
- *
18790
- * Clamped on the way out, so what this reports is the page it drew: a
18791
- * bookmarked link to page 99 of a result that has since shrunk reads back as
18792
- * the last page rather than as 99.
18793
18852
  * - `page-size`/`pageSize`: How many rows a page holds. Zero is every row, which is what "no paging" means.
18794
18853
  *
18795
18854
  * **It announces nothing, and that is not an omission.** Nothing in this
@@ -18803,6 +18862,23 @@ export type CustomElements = {
18803
18862
  * nothing about the result. Ignored in `client` mode rather than trusted,
18804
18863
  * because two sources for one count is the mistake, and here the array is the
18805
18864
  * one that cannot be stale.
18865
+ * - `windowed`: Draws the rows in view and stands empty space in for the rest.
18866
+ *
18867
+ * **Not a `mode`.** `mode` says where the rows come from and who filtered
18868
+ * them; this says how many of the ones we hold get drawn. They are
18869
+ * independent: a consumer fetching five thousand rows a chunk at a time wants
18870
+ * `mode="server"` and this at once, and folding this into `mode` would be an
18871
+ * API that forbids that combination for no reason.
18872
+ *
18873
+ * **Composes with `paginated` rather than excluding it.** A window over a
18874
+ * page is coherent -- the page is the slice of the result, the window is the
18875
+ * part of that slice on screen -- and the arithmetic is the same either way,
18876
+ * so there is nothing to forbid. Together they suit a table paging at a
18877
+ * thousand rows, where the page is a reasonable fetch and a poor render.
18878
+ *
18879
+ * Off by default. Windowing costs a scroll listener, a measurement per row
18880
+ * drawn, and a `block-size` on the scrolling box, and a table of twenty rows
18881
+ * should pay none of them.
18806
18882
  * - `paginated`: Whether to draw a pagination under the grid.
18807
18883
  *
18808
18884
  * On by default once a page size is set, because a paged table with no way to
@@ -18885,6 +18961,8 @@ export type CustomElements = {
18885
18961
  *
18886
18962
  * Custom selectors for styling elements within the component.
18887
18963
  *
18964
+ * - `after`: The room the rows below the window would have taken. Windowed only.
18965
+ * - `before`: The room the rows above the window would have taken. Windowed only.
18888
18966
  * - `cell`: One cell.
18889
18967
  * - `empty`: Shown when no rows match.
18890
18968
  * - `footer`: The region below the grid.
@@ -32967,6 +33045,18 @@ export type CustomElementsSolidJs = {
32967
33045
  * - `default-page`/`defaultPage`: Page to open on, where nothing declared one.
32968
33046
  * - `default-page-size`/`defaultPageSize`: Page size to open with, where nothing declared one.
32969
33047
  * - `default-sort`/`defaultSort`: Order to open on, where nothing declared one.
33048
+ * - `estimate`: How tall a row is taken to be before it has been drawn, in pixels.
33049
+ *
33050
+ * Only read while `windowed`, and a starting guess rather than a claim: a
33051
+ * cell that wraps makes its row taller than its neighbors, and the window
33052
+ * replaces this with what each row came to as it draws. It still has to be
33053
+ * close, because it is what the scrollbar is built from before anything has
33054
+ * been measured.
33055
+ * - `sort`: The order, as `field:direction`.
33056
+ *
33057
+ * Set it and this element defers: a reader's click fires `helix-sort` and
33058
+ * changes nothing until a new value arrives. `default-sort` seeds an order this
33059
+ * element then owns.
32970
33060
  * - `key`: The field naming a row's stable identity.
32971
33061
  *
32972
33062
  * **Needed before selection exists, not after.** Lit keys the rendered rows by
@@ -32975,17 +33065,17 @@ export type CustomElementsSolidJs = {
32975
33065
  * attached to the row it belongs to. Left unset the index is the key, which is
32976
33066
  * correct only while nothing reorders: it is why this defaults to something
32977
33067
  * that works and is worth setting anyway.
32978
- * - `sort`: The order, as `field:direction`.
32979
- *
32980
- * Set it and this element defers: a reader's click fires `helix-sort` and
32981
- * changes nothing until a new value arrives. `default-sort` seeds an order this
32982
- * element then owns.
32983
- * - `label`: Accessible name for the grid.
32984
33068
  * - `filter`: What the rows are narrowed to, as one query across every column.
32985
33069
  *
32986
33070
  * One query rather than a predicate per column, which is what a reader means
32987
33071
  * by a search box above a table. It matches on each column's ordering value,
32988
33072
  * so search and sort cannot disagree about what a column holds.
33073
+ * - `label`: Accessible name for the grid.
33074
+ * - `page`: The page on show, from 1.
33075
+ *
33076
+ * Clamped on the way out, so what this reports is the page it drew: a
33077
+ * bookmarked link to page 99 of a result that has since shrunk reads back as
33078
+ * the last page rather than as 99.
32989
33079
  * - `mode`: Whether this element does the work or asks the consumer to.
32990
33080
  *
32991
33081
  * `client` filters, sorts and pages `rows` itself. `server` applies none of
@@ -32994,11 +33084,6 @@ export type CustomElementsSolidJs = {
32994
33084
  * the client cannot hold the data, so this is not an optimization, it is the
32995
33085
  * only mode that works, and it is here from the start because adding it later
32996
33086
  * inverts the data flow.
32997
- * - `page`: The page on show, from 1.
32998
- *
32999
- * Clamped on the way out, so what this reports is the page it drew: a
33000
- * bookmarked link to page 99 of a result that has since shrunk reads back as
33001
- * the last page rather than as 99.
33002
33087
  * - `page-size`/`pageSize`: How many rows a page holds. Zero is every row, which is what "no paging" means.
33003
33088
  *
33004
33089
  * **It announces nothing, and that is not an omission.** Nothing in this
@@ -33012,6 +33097,23 @@ export type CustomElementsSolidJs = {
33012
33097
  * nothing about the result. Ignored in `client` mode rather than trusted,
33013
33098
  * because two sources for one count is the mistake, and here the array is the
33014
33099
  * one that cannot be stale.
33100
+ * - `windowed`: Draws the rows in view and stands empty space in for the rest.
33101
+ *
33102
+ * **Not a `mode`.** `mode` says where the rows come from and who filtered
33103
+ * them; this says how many of the ones we hold get drawn. They are
33104
+ * independent: a consumer fetching five thousand rows a chunk at a time wants
33105
+ * `mode="server"` and this at once, and folding this into `mode` would be an
33106
+ * API that forbids that combination for no reason.
33107
+ *
33108
+ * **Composes with `paginated` rather than excluding it.** A window over a
33109
+ * page is coherent -- the page is the slice of the result, the window is the
33110
+ * part of that slice on screen -- and the arithmetic is the same either way,
33111
+ * so there is nothing to forbid. Together they suit a table paging at a
33112
+ * thousand rows, where the page is a reasonable fetch and a poor render.
33113
+ *
33114
+ * Off by default. Windowing costs a scroll listener, a measurement per row
33115
+ * drawn, and a `block-size` on the scrolling box, and a table of twenty rows
33116
+ * should pay none of them.
33015
33117
  * - `paginated`: Whether to draw a pagination under the grid.
33016
33118
  *
33017
33119
  * On by default once a page size is set, because a paged table with no way to
@@ -33094,6 +33196,8 @@ export type CustomElementsSolidJs = {
33094
33196
  *
33095
33197
  * Custom selectors for styling elements within the component.
33096
33198
  *
33199
+ * - `after`: The room the rows below the window would have taken. Windowed only.
33200
+ * - `before`: The room the rows above the window would have taken. Windowed only.
33097
33201
  * - `cell`: One cell.
33098
33202
  * - `empty`: Shown when no rows match.
33099
33203
  * - `footer`: The region below the grid.