@fusion.dev/helix-headless 0.5.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.
- package/custom-elements.json +269 -32
- package/dist/data-table/root/data-table-root.d.ts +138 -28
- package/dist/data-table/root/data-table-root.js +110 -21
- package/dist/data-table/root/order.d.ts +25 -0
- package/dist/data-table/root/order.js +6 -3
- package/dist/virtual-list/root/virtual-list-root.d.ts +23 -0
- package/dist/virtual-list/root/virtual-list-root.js +13 -0
- package/package.json +5 -5
- package/types/jsx.d.ts +198 -52
- package/types/vue.d.ts +52 -15
|
@@ -46,6 +46,8 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
|
|
|
46
46
|
* @event {HelixNavigateEvent} helix-navigate - Fired when a reader asks for another page, and cancelable.
|
|
47
47
|
* @event {HelixSortEvent} helix-sort - Fired when a reader asks for a different order, and cancelable.
|
|
48
48
|
*
|
|
49
|
+
* @csspart after - The room the rows below the window would have taken. Windowed only.
|
|
50
|
+
* @csspart before - The room the rows above the window would have taken. Windowed only.
|
|
49
51
|
* @csspart cell - One cell.
|
|
50
52
|
* @csspart empty - Shown when no rows match.
|
|
51
53
|
* @csspart footer - The region below the grid.
|
|
@@ -86,16 +88,15 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
86
88
|
*/
|
|
87
89
|
defaultSort: string;
|
|
88
90
|
/**
|
|
89
|
-
*
|
|
91
|
+
* How tall a row is taken to be before it has been drawn, in pixels.
|
|
90
92
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* that works and is worth setting anyway.
|
|
93
|
+
* Only read while `windowed`, and a starting guess rather than a claim: a
|
|
94
|
+
* cell that wraps makes its row taller than its neighbors, and the window
|
|
95
|
+
* replaces this with what each row came to as it draws. It still has to be
|
|
96
|
+
* close, because it is what the scrollbar is built from before anything has
|
|
97
|
+
* been measured.
|
|
97
98
|
*/
|
|
98
|
-
|
|
99
|
+
estimate: number;
|
|
99
100
|
/**
|
|
100
101
|
* The order, as `field:direction`.
|
|
101
102
|
*
|
|
@@ -106,9 +107,16 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
106
107
|
get sort(): string;
|
|
107
108
|
set sort(next: string);
|
|
108
109
|
/**
|
|
109
|
-
*
|
|
110
|
+
* The field naming a row's stable identity.
|
|
111
|
+
*
|
|
112
|
+
* **Needed before selection exists, not after.** Lit keys the rendered rows by
|
|
113
|
+
* it, so a sort moves a row's element rather than rewriting every cell in
|
|
114
|
+
* place, which is what keeps focus inside a cell and keeps a CSS transition
|
|
115
|
+
* attached to the row it belongs to. Left unset the index is the key, which is
|
|
116
|
+
* correct only while nothing reorders: it is why this defaults to something
|
|
117
|
+
* that works and is worth setting anyway.
|
|
110
118
|
*/
|
|
111
|
-
|
|
119
|
+
key: string;
|
|
112
120
|
/**
|
|
113
121
|
* What the rows are narrowed to, as one query across every column.
|
|
114
122
|
*
|
|
@@ -119,16 +127,9 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
119
127
|
get filter(): string;
|
|
120
128
|
set filter(next: string);
|
|
121
129
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* `client` filters, sorts and pages `rows` itself. `server` applies none of
|
|
125
|
-
* them: `rows` is the page, `total` is the size of the whole result, and the
|
|
126
|
-
* three events are requests the consumer answers by fetching. Past some size
|
|
127
|
-
* the client cannot hold the data, so this is not an optimization, it is the
|
|
128
|
-
* only mode that works, and it is here from the start because adding it later
|
|
129
|
-
* inverts the data flow.
|
|
130
|
+
* Accessible name for the grid.
|
|
130
131
|
*/
|
|
131
|
-
|
|
132
|
+
label: string;
|
|
132
133
|
/**
|
|
133
134
|
* The page on show, from 1.
|
|
134
135
|
*
|
|
@@ -138,6 +139,17 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
138
139
|
*/
|
|
139
140
|
get page(): number;
|
|
140
141
|
set page(next: number);
|
|
142
|
+
/**
|
|
143
|
+
* Whether this element does the work or asks the consumer to.
|
|
144
|
+
*
|
|
145
|
+
* `client` filters, sorts and pages `rows` itself. `server` applies none of
|
|
146
|
+
* them: `rows` is the page, `total` is the size of the whole result, and the
|
|
147
|
+
* three events are requests the consumer answers by fetching. Past some size
|
|
148
|
+
* the client cannot hold the data, so this is not an optimization, it is the
|
|
149
|
+
* only mode that works, and it is here from the start because adding it later
|
|
150
|
+
* inverts the data flow.
|
|
151
|
+
*/
|
|
152
|
+
mode: 'client' | 'server';
|
|
141
153
|
/**
|
|
142
154
|
* The whole result, in the order it arrived.
|
|
143
155
|
*
|
|
@@ -170,6 +182,26 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
170
182
|
* one that cannot be stale.
|
|
171
183
|
*/
|
|
172
184
|
total: number;
|
|
185
|
+
/**
|
|
186
|
+
* Draws the rows in view and stands empty space in for the rest.
|
|
187
|
+
*
|
|
188
|
+
* **Not a `mode`.** `mode` says where the rows come from and who filtered
|
|
189
|
+
* them; this says how many of the ones we hold get drawn. They are
|
|
190
|
+
* independent: a consumer fetching five thousand rows a chunk at a time wants
|
|
191
|
+
* `mode="server"` and this at once, and folding this into `mode` would be an
|
|
192
|
+
* API that forbids that combination for no reason.
|
|
193
|
+
*
|
|
194
|
+
* **Composes with `paginated` rather than excluding it.** A window over a
|
|
195
|
+
* page is coherent -- the page is the slice of the result, the window is the
|
|
196
|
+
* part of that slice on screen -- and the arithmetic is the same either way,
|
|
197
|
+
* so there is nothing to forbid. Together they suit a table paging at a
|
|
198
|
+
* thousand rows, where the page is a reasonable fetch and a poor render.
|
|
199
|
+
*
|
|
200
|
+
* Off by default. Windowing costs a scroll listener, a measurement per row
|
|
201
|
+
* drawn, and a `block-size` on the scrolling box, and a table of twenty rows
|
|
202
|
+
* should pay none of them.
|
|
203
|
+
*/
|
|
204
|
+
windowed: boolean;
|
|
173
205
|
/**
|
|
174
206
|
* Whether to draw a pagination under the grid.
|
|
175
207
|
*
|
|
@@ -185,6 +217,46 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
185
217
|
private pageState;
|
|
186
218
|
private pageSizeState;
|
|
187
219
|
private paginatedState;
|
|
220
|
+
private readonly virtualize;
|
|
221
|
+
/**
|
|
222
|
+
* The rows in the shadow tree, each with the row of the page it drew.
|
|
223
|
+
*
|
|
224
|
+
* **Read off `data-index` rather than counted.** `hostUpdated` runs before
|
|
225
|
+
* `updated`, so what is in the tree when the controller asks is the previous
|
|
226
|
+
* window; counting from the current one would record the right heights
|
|
227
|
+
* against the wrong rows every time the window moved.
|
|
228
|
+
*/
|
|
229
|
+
private drawn;
|
|
230
|
+
/**
|
|
231
|
+
* How many rows there are to window over: the page, or the result.
|
|
232
|
+
*
|
|
233
|
+
* **Read off the held arrangement rather than counted afresh**, because the
|
|
234
|
+
* controller asks this on every scroll and `matched` walks the whole result
|
|
235
|
+
* whenever a filter is on. Held, a scroll through a filtered table of five
|
|
236
|
+
* thousand rows costs a lookup; counted, it costs five thousand comparisons a
|
|
237
|
+
* frame, which is the cost windowing was added to avoid.
|
|
238
|
+
*
|
|
239
|
+
* `matched` still answers where nothing is windowing, which is the path that
|
|
240
|
+
* holds nothing and should not start.
|
|
241
|
+
*/
|
|
242
|
+
private get available();
|
|
243
|
+
/**
|
|
244
|
+
* The arranged result, kept between renders.
|
|
245
|
+
*
|
|
246
|
+
* **A window moves and the order does not.** Every scroll that reveals a row
|
|
247
|
+
* re-renders, and deriving that render from `rows` would filter and sort the
|
|
248
|
+
* whole result again each time: 23.7ms at five thousand rows, three frames
|
|
249
|
+
* spent before a cell is drawn. Held against the inputs that change it, a
|
|
250
|
+
* scroll costs a slice.
|
|
251
|
+
*
|
|
252
|
+
* Only the windowed path uses this. A paged table re-renders when the reader
|
|
253
|
+
* asks for a page, which is rare enough that holding a second copy of the
|
|
254
|
+
* result to save a sort is the wrong trade.
|
|
255
|
+
*/
|
|
256
|
+
private held;
|
|
257
|
+
private arrange;
|
|
258
|
+
/** The part of a page the reader can see, which is every row when not windowed. */
|
|
259
|
+
private windowOf;
|
|
188
260
|
/** The columns declared as children, in the order they are written. */
|
|
189
261
|
get tableColumns(): HelixHeadlessTableColumn[];
|
|
190
262
|
/** The columns reduced to what the ordering needs. */
|
|
@@ -212,15 +284,26 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
212
284
|
* which means the out-of-the-box arrangement is the unbounded one and nothing
|
|
213
285
|
* about it says that a result of any size will simply be drawn.
|
|
214
286
|
*
|
|
215
|
-
* **Windowing was refused
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
287
|
+
* **Windowing was refused here, and is now `windowed`.** The refusal is kept
|
|
288
|
+
* because most of it was right, and because what changed is not that somebody
|
|
289
|
+
* disagreed with it. It rested on four claims: that a third mechanism would
|
|
290
|
+
* fight `paginated` and `mode="server"`; that a grid has rows of different
|
|
291
|
+
* heights; that it needs a sticky header; and that `aria-rowindex` has to keep
|
|
292
|
+
* describing a result the element no longer holds in full.
|
|
293
|
+
*
|
|
294
|
+
* Three stopped being true. `VirtualizeController` learnt to measure each row
|
|
295
|
+
* and place the next by what the one above came to, which is the variable
|
|
296
|
+
* heights. `aria-rowindex` was already result-relative because paging needed
|
|
297
|
+
* it, so a window is a second offset on arithmetic that existed. And the
|
|
298
|
+
* sticky header is one `position: sticky` on the heading cells rather than on
|
|
299
|
+
* `[part="head"]`, which is `display: contents` and generates no box to stick
|
|
300
|
+
* -- that is why it looked hard and is not.
|
|
301
|
+
*
|
|
302
|
+
* The first was avoided rather than answered. `windowed` is not a `mode` and
|
|
303
|
+
* does not exclude `paginated`: it says how much of what we hold gets drawn,
|
|
304
|
+
* where `mode` says where it came from, so there is no third axis to fight the
|
|
305
|
+
* other two. What survives is the cost, which is why windowing is off by
|
|
306
|
+
* default and why this warning still fires for a table that has none of them.
|
|
224
307
|
*
|
|
225
308
|
* Warned once per element, in a browser only: a warning during a server render
|
|
226
309
|
* is one nobody can act on and one that would print per request.
|
|
@@ -327,8 +410,35 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
|
|
|
327
410
|
* reason a virtualized or paged grid needs `aria-rowindex` at all: a screen
|
|
328
411
|
* reader on page three should hear row 41, and a grid whose indices restart at
|
|
329
412
|
* every page tells a reader the result is twenty rows long however many it has.
|
|
413
|
+
*
|
|
414
|
+
* A window is the second offset and stacks on the page: with both on, row one
|
|
415
|
+
* of the window is the page's start plus how far down the page it begins.
|
|
330
416
|
*/
|
|
331
417
|
private rowIndex;
|
|
418
|
+
/**
|
|
419
|
+
* What decides which row is at which index, as one comparable value.
|
|
420
|
+
*
|
|
421
|
+
* Not the rows themselves: those are compared by identity beside this, and a
|
|
422
|
+
* consumer handing back an equal array is handing back the same rows in the
|
|
423
|
+
* same order whatever object it is.
|
|
424
|
+
*/
|
|
425
|
+
private get placement();
|
|
426
|
+
/** The signature the recorded heights were measured against. */
|
|
427
|
+
private measuredAgainst;
|
|
428
|
+
/** Where the window begins in the page, which is the top when there is none. */
|
|
429
|
+
private get windowStart();
|
|
430
|
+
/**
|
|
431
|
+
* The room the rows outside the window would have taken.
|
|
432
|
+
*
|
|
433
|
+
* **`role="presentation"`, because this is geometry standing inside a
|
|
434
|
+
* `rowgroup`.** A bare `div` there is a child the role does not allow, and a
|
|
435
|
+
* screen reader walking the rows would meet something that is not one. The
|
|
436
|
+
* room itself is what keeps the scrollbar honest: a box that scrolls twenty
|
|
437
|
+
* rows while claiming ten thousand gives a thumb the size of its track.
|
|
438
|
+
*/
|
|
439
|
+
private renderSpacer;
|
|
440
|
+
/** The controller owns the geometry; this is only what tells it to look again. */
|
|
441
|
+
private readonly handleScroll;
|
|
332
442
|
protected render(): TemplateResult;
|
|
333
443
|
/**
|
|
334
444
|
* Listens on the host, not on the slot the columns are assigned to.
|
|
@@ -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 {
|
|
180
|
+
return {
|
|
181
|
+
available: this.rows.length,
|
|
182
|
+
matched: total,
|
|
183
|
+
rows: this.windowOf([...this.rows]),
|
|
184
|
+
total,
|
|
185
|
+
};
|
|
135
186
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
290
|
-
return
|
|
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,
|
|
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)}
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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
|
|
71
|
-
|
|
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
|
};
|
|
@@ -32,6 +32,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
32
32
|
export declare class HelixHeadlessVirtualList extends HelixElement {
|
|
33
33
|
/**
|
|
34
34
|
* How tall one row is, in pixels, used to map scroll offset to an index.
|
|
35
|
+
*
|
|
36
|
+
* An estimate rather than a measurement while `measured` is off, which is
|
|
37
|
+
* right whenever the rows really are one height and is the cheaper of the two.
|
|
35
38
|
*/
|
|
36
39
|
estimate: number;
|
|
37
40
|
/**
|
|
@@ -42,6 +45,16 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
|
|
|
42
45
|
* What the list is called, since the rendered rows are only a window on it.
|
|
43
46
|
*/
|
|
44
47
|
label: string;
|
|
48
|
+
/**
|
|
49
|
+
* Places rows by what they came to rather than by `estimate`.
|
|
50
|
+
*
|
|
51
|
+
* **Off by default, because the straight line is better when it holds.** One
|
|
52
|
+
* estimate makes offset and index convert by division, which can never
|
|
53
|
+
* disagree with itself; measuring is what a set of genuinely different
|
|
54
|
+
* heights needs and costs a read of each row as it renders. A list of
|
|
55
|
+
* one-line rows should leave this alone.
|
|
56
|
+
*/
|
|
57
|
+
measured: boolean;
|
|
45
58
|
/**
|
|
46
59
|
* The parsed source.
|
|
47
60
|
*
|
|
@@ -49,6 +62,16 @@ export declare class HelixHeadlessVirtualList extends HelixElement {
|
|
|
49
62
|
*/
|
|
50
63
|
private rows;
|
|
51
64
|
private readonly virtualize;
|
|
65
|
+
/**
|
|
66
|
+
* The rows in the tree, each with the item it drew.
|
|
67
|
+
*
|
|
68
|
+
* **Read off `data-index` rather than counted**, because what is in the tree
|
|
69
|
+
* when the controller asks is what `place` appended on the *previous* pass:
|
|
70
|
+
* `hostUpdated` runs before `updated`, so counting from the current window
|
|
71
|
+
* would record the right heights against the wrong items every time the
|
|
72
|
+
* window moved.
|
|
73
|
+
*/
|
|
74
|
+
private drawn;
|
|
52
75
|
/**
|
|
53
76
|
* Reads the source named by `for`, once the document exists to read.
|
|
54
77
|
*
|
|
@@ -13,13 +13,22 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
|
|
|
13
13
|
this.estimate = 32;
|
|
14
14
|
this.for = '';
|
|
15
15
|
this.label = '';
|
|
16
|
+
this.measured = false;
|
|
16
17
|
this.rows = [];
|
|
17
18
|
this.virtualize = new VirtualizeController(this, {
|
|
18
19
|
container: () => this.renderRoot.querySelector('[part="viewport"]'),
|
|
19
20
|
count: () => this.rows.length,
|
|
20
21
|
estimate: () => this.estimate,
|
|
22
|
+
measured: () => this.measured,
|
|
23
|
+
rows: () => this.drawn(),
|
|
21
24
|
});
|
|
22
25
|
}
|
|
26
|
+
drawn() {
|
|
27
|
+
return [...this.querySelectorAll('[data-index]')].map((element) => ({
|
|
28
|
+
element,
|
|
29
|
+
index: Number(element.dataset.index),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
23
32
|
firstUpdated(changed) {
|
|
24
33
|
super.firstUpdated?.(changed);
|
|
25
34
|
this.read();
|
|
@@ -73,6 +82,7 @@ let HelixHeadlessVirtualList = class HelixHeadlessVirtualList extends HelixEleme
|
|
|
73
82
|
element.setAttribute('role', 'listitem');
|
|
74
83
|
element.setAttribute('aria-posinset', String(at + 1));
|
|
75
84
|
element.setAttribute('aria-setsize', String(total));
|
|
85
|
+
element.setAttribute('data-index', String(at));
|
|
76
86
|
}
|
|
77
87
|
fragment.append(clone);
|
|
78
88
|
}
|
|
@@ -102,6 +112,9 @@ __decorate([
|
|
|
102
112
|
__decorate([
|
|
103
113
|
property()
|
|
104
114
|
], HelixHeadlessVirtualList.prototype, "label", void 0);
|
|
115
|
+
__decorate([
|
|
116
|
+
property({ type: Boolean })
|
|
117
|
+
], HelixHeadlessVirtualList.prototype, "measured", void 0);
|
|
105
118
|
__decorate([
|
|
106
119
|
state()
|
|
107
120
|
], HelixHeadlessVirtualList.prototype, "rows", void 0);
|
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.
|
|
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.
|
|
49
|
-
"@fusion.dev/helix-icons": "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.
|
|
55
|
-
"@fusion.dev/helix-testing": "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",
|