@fusion.dev/helix-headless 0.166.0 → 0.168.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 +2658 -481
- package/dist/canvas/arrange.d.ts +32 -0
- package/dist/canvas/arrange.js +144 -0
- package/dist/canvas/paste.d.ts +25 -0
- package/dist/canvas/paste.js +58 -0
- package/dist/canvas/root/canvas-root.d.ts +60 -3
- package/dist/canvas/root/canvas-root.js +166 -2
- package/dist/chat/composer/chat-composer.js +5 -1
- package/dist/combobox/root/combobox-root.d.ts +6 -1
- package/dist/combobox/root/combobox-root.js +2 -2
- package/dist/date-input/root/date-input-root.d.ts +1 -1
- package/dist/element-events.generated.js +2 -0
- package/dist/file-input/root/file-input-root.d.ts +1 -1
- package/dist/file-input/root/file-input-root.js +1 -1
- package/dist/format-date/root/format-date-root.d.ts +10 -3
- package/dist/format-date/root/format-date-root.js +6 -1
- package/dist/listbox/root/listbox-root.d.ts +12 -1
- package/dist/listbox/root/listbox-root.js +13 -1
- package/dist/select/root/select-root.js +1 -1
- package/dist/table/grid-controller.d.ts +10 -0
- package/dist/table/grid-controller.js +7 -0
- package/dist/table/root/table-root.d.ts +71 -1
- package/dist/table/root/table-root.js +122 -7
- package/dist/table/row/table-row.d.ts +17 -0
- package/dist/table/row/table-row.js +23 -0
- package/dist/time-input/root/time-input-root.d.ts +2 -1
- package/dist/time-input/root/time-input-root.js +1 -1
- package/dist/time-picker/root/time-picker-root.d.ts +48 -1
- package/dist/time-picker/root/time-picker-root.js +79 -21
- package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
- package/dist/virtual-list/root/virtual-list-root.js +216 -32
- package/dist/virtual-list/sections.d.ts +27 -0
- package/dist/virtual-list/sections.js +43 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +454 -76
- package/types/vue.d.ts +89 -7
- package/types/words.d.ts +27 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AriaForwardController, HelixFormElement, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
import '../../listbox/index.js';
|
|
4
|
-
import '../../tag/index.js';
|
|
4
|
+
import '../../tag/root/index.js';
|
|
5
5
|
declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
6
6
|
/**
|
|
7
7
|
* @summary A text field that filters a list of options as you type.
|
|
@@ -668,6 +668,11 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
|
|
|
668
668
|
* slot is for the page and the method is for whoever is building the styled
|
|
669
669
|
* layer, and a fallback with only the first is one a tier can restyle and
|
|
670
670
|
* cannot change.
|
|
671
|
+
*
|
|
672
|
+
* **One em square by its own attributes**, which any stylesheet overrides.
|
|
673
|
+
* An inline `<svg>` with a `viewBox` and no size fills the width it is given:
|
|
674
|
+
* measured with no stylesheet, the caret was 1,264px tall, the field 1,289px,
|
|
675
|
+
* and the panel opened above the top of the page.
|
|
671
676
|
*/
|
|
672
677
|
protected renderIndicator(): TemplateResult;
|
|
673
678
|
/**
|
|
@@ -13,7 +13,7 @@ import { html } from 'lit/static-html.js';
|
|
|
13
13
|
import { optionsAmong } from '../../option/root/option-root.js';
|
|
14
14
|
import { HelixHeadlessTag } from '../../tag/root/tag-root.js';
|
|
15
15
|
import '../../listbox/index.js';
|
|
16
|
-
import '../../tag/index.js';
|
|
16
|
+
import '../../tag/root/index.js';
|
|
17
17
|
const isInsertion = (event) => !event.inputType?.startsWith('delete');
|
|
18
18
|
let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
|
|
19
19
|
constructor() {
|
|
@@ -612,7 +612,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
612
612
|
}
|
|
613
613
|
renderIndicator() {
|
|
614
614
|
return html `<slot name="indicator">
|
|
615
|
-
<svg part="indicator" fill="currentColor" viewBox=${caretDown.viewBox} aria-hidden="true">
|
|
615
|
+
<svg part="indicator" fill="currentColor" width="1em" height="1em" viewBox=${caretDown.viewBox} aria-hidden="true">
|
|
616
616
|
${unsafeSVG(caretDown.content)}
|
|
617
617
|
</svg>
|
|
618
618
|
</slot>`;
|
|
@@ -51,7 +51,7 @@ declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fus
|
|
|
51
51
|
* | Page Up, Page Down on a segment | Steps by a page: a week of days, two months, or five years. |
|
|
52
52
|
* | Home, End on a segment | Goes to the segment's first or last value. |
|
|
53
53
|
* | Digits on a segment | Fill it, and move to the next once it can take no more. |
|
|
54
|
-
* | Backspace, Delete on a segment |
|
|
54
|
+
* | Backspace, Delete on a segment | Take off its last digit, or move to the segment before where it is empty. |
|
|
55
55
|
* | Left, Right on a segment | Move to the previous or the next segment, along the line of text. |
|
|
56
56
|
*
|
|
57
57
|
* In the calendar, the keys are `DatePicker`'s: the arrows move by a day or a
|
|
@@ -17,6 +17,7 @@ registerElementEvents({
|
|
|
17
17
|
'helix-headless-callout': ['helix-dismiss'],
|
|
18
18
|
'helix-headless-canvas': [
|
|
19
19
|
'helix-before-move',
|
|
20
|
+
'helix-before-paste',
|
|
20
21
|
'helix-before-reconnect',
|
|
21
22
|
'helix-before-relabel',
|
|
22
23
|
'helix-canvas-edit',
|
|
@@ -25,6 +26,7 @@ registerElementEvents({
|
|
|
25
26
|
'helix-canvas-zoom',
|
|
26
27
|
'helix-connect',
|
|
27
28
|
'helix-move',
|
|
29
|
+
'helix-pasted',
|
|
28
30
|
'helix-reconnect',
|
|
29
31
|
'helix-relabel',
|
|
30
32
|
'helix-remove',
|
|
@@ -2,7 +2,7 @@ import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core'
|
|
|
2
2
|
import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
import { type FileStatus } from '../status.js';
|
|
4
4
|
export type { FileStatus } from '../status.js';
|
|
5
|
-
import '../../tag/index.js';
|
|
5
|
+
import '../../tag/root/index.js';
|
|
6
6
|
declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
7
7
|
/**
|
|
8
8
|
* @summary A field for choosing files from a picker or by dropping them on it.
|
|
@@ -11,7 +11,7 @@ import { property, state } from 'lit/decorators.js';
|
|
|
11
11
|
import { html } from 'lit/static-html.js';
|
|
12
12
|
import { screenFiles } from '../screen.js';
|
|
13
13
|
import { barOf, saysStatus } from '../status.js';
|
|
14
|
-
import '../../tag/index.js';
|
|
14
|
+
import '../../tag/root/index.js';
|
|
15
15
|
const size = (bytes, locale, inBytes) => {
|
|
16
16
|
const units = ['', 'KB', 'MB', 'GB', 'TB'];
|
|
17
17
|
let value = bytes;
|
|
@@ -84,7 +84,8 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
84
84
|
*/
|
|
85
85
|
calendar?: DateCalendar | MonthCalendar;
|
|
86
86
|
/**
|
|
87
|
-
* The timestamp,
|
|
87
|
+
* The timestamp: a `Date`, milliseconds since the epoch, or a string in any
|
|
88
|
+
* format `Date` can parse. The attribute is the string.
|
|
88
89
|
*
|
|
89
90
|
* An ISO string is what an API sends and what `<time datetime>` takes, so it
|
|
90
91
|
* is the shape that needs no translation at either end. Left unset this
|
|
@@ -100,7 +101,7 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
100
101
|
* it. An element rendered on a server needs the attribute; one rendered in a
|
|
101
102
|
* browser may use either.
|
|
102
103
|
*/
|
|
103
|
-
date: string;
|
|
104
|
+
date: Date | number | string;
|
|
104
105
|
/**
|
|
105
106
|
* The locale's own pattern for the date: `full`, `long`, `medium`, or
|
|
106
107
|
* `short`. Set, it stands in for `weekday`, `day`, `month`, `year` and `era`.
|
|
@@ -259,7 +260,13 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
259
260
|
private get dateOnly();
|
|
260
261
|
/** The timestamp, or nothing where there is not one to read. */
|
|
261
262
|
private get moment();
|
|
262
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* The timestamp as written, in the property, the attribute or the light DOM.
|
|
265
|
+
*
|
|
266
|
+
* A `Date` or a number is written as its ISO string, so it is read the way an
|
|
267
|
+
* attribute is. Measured before this: assigning `new Date()` threw, since
|
|
268
|
+
* only a string has `trim`, where Shoelace and Web Awesome take a `Date`.
|
|
269
|
+
*/
|
|
263
270
|
private get source();
|
|
264
271
|
/** Whether the reading is how long ago rather than the date. */
|
|
265
272
|
private get reading();
|
|
@@ -44,7 +44,12 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
44
44
|
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
45
45
|
}
|
|
46
46
|
get source() {
|
|
47
|
-
|
|
47
|
+
const date = this.date;
|
|
48
|
+
if (typeof date === 'string') {
|
|
49
|
+
return (date || this.written).trim();
|
|
50
|
+
}
|
|
51
|
+
const at = new Date(date);
|
|
52
|
+
return Number.isNaN(at.getTime()) ? '' : at.toISOString();
|
|
48
53
|
}
|
|
49
54
|
get reading() {
|
|
50
55
|
return this.elapsed || this.relative !== undefined;
|
|
@@ -542,7 +542,8 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
|
|
|
542
542
|
*/
|
|
543
543
|
navigate(event: KeyboardEvent): void;
|
|
544
544
|
/**
|
|
545
|
-
* Scrolls the chosen option to the middle of the list
|
|
545
|
+
* Scrolls the chosen option to the middle of the list, or the highlighted one
|
|
546
|
+
* where nothing is chosen.
|
|
546
547
|
*
|
|
547
548
|
* A method rather than something this does on its own, because a listbox
|
|
548
549
|
* opening on its answer usually wants it merely *visible* and the browser
|
|
@@ -554,6 +555,16 @@ export declare class HelixHeadlessListbox extends HelixHeadlessListbox_base {
|
|
|
554
555
|
* arithmetic, since the chosen option may not be drawn.
|
|
555
556
|
*/
|
|
556
557
|
reveal(): void;
|
|
558
|
+
/**
|
|
559
|
+
* Puts the highlight on the option holding `value`, without choosing it, so
|
|
560
|
+
* the arrow keys move from there. Nothing changes where no option a reader
|
|
561
|
+
* can reach holds `value`.
|
|
562
|
+
*
|
|
563
|
+
* Time Picker calls it for a time between two of its steps, which no option
|
|
564
|
+
* holds: without it the first arrow moved from the top of the list, so Down
|
|
565
|
+
* from 14:07 went to midnight.
|
|
566
|
+
*/
|
|
567
|
+
highlightValue(value: string): void;
|
|
557
568
|
/** Hides what the filter excludes, and writes the rest onto the options. */
|
|
558
569
|
private place;
|
|
559
570
|
/**
|
|
@@ -391,11 +391,23 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
|
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
393
|
const items = this.partNamed('items');
|
|
394
|
-
const chosen = this.options.find((option) => option.selected && !option.hidden)
|
|
394
|
+
const chosen = this.options.find((option) => option.selected && !option.hidden) ??
|
|
395
|
+
this.options.find((option) => option.value === this.activeValue && !option.hidden);
|
|
395
396
|
if (items && chosen) {
|
|
396
397
|
centerInView(items, chosen);
|
|
397
398
|
}
|
|
398
399
|
}
|
|
400
|
+
highlightValue(value) {
|
|
401
|
+
if (!this.shown.some((option) => !option.disabled && option.value === value)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.typeahead.reset();
|
|
405
|
+
this.parked = false;
|
|
406
|
+
this.activeValue = value;
|
|
407
|
+
if (this.childrenKnown) {
|
|
408
|
+
this.place();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
399
411
|
place() {
|
|
400
412
|
this.forgetRemoved();
|
|
401
413
|
const typed = this.filter.trim();
|
|
@@ -340,7 +340,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
340
340
|
}
|
|
341
341
|
renderIndicator() {
|
|
342
342
|
return html `<slot name="indicator">
|
|
343
|
-
<svg part="indicator" fill="currentColor" viewBox=${caretDown.viewBox} aria-hidden="true">
|
|
343
|
+
<svg part="indicator" fill="currentColor" width="1em" height="1em" viewBox=${caretDown.viewBox} aria-hidden="true">
|
|
344
344
|
${unsafeSVG(caretDown.content)}
|
|
345
345
|
</svg>
|
|
346
346
|
</slot>`;
|
|
@@ -51,6 +51,16 @@ export interface TableGridOptions {
|
|
|
51
51
|
disclose?: (row: number, forward: boolean) => boolean | number;
|
|
52
52
|
/** Whether the table is a grid now. */
|
|
53
53
|
on: () => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The rows drawn now, in a table that draws only some of them, which are
|
|
56
|
+
* the only rows whose cells are marked. Every row where it is left out.
|
|
57
|
+
*/
|
|
58
|
+
drawn?: () => ReadonlySet<Element> | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Brings drawn row `row` into view before a key focuses it, in a table that
|
|
61
|
+
* draws only the rows in view, so the row is drawn by then.
|
|
62
|
+
*/
|
|
63
|
+
reveal?: (row: number) => void;
|
|
54
64
|
}
|
|
55
65
|
/**
|
|
56
66
|
* The keyboard of a `grid` table: one tab stop among its cells, the arrow
|
|
@@ -247,6 +247,9 @@ export class TableGridController {
|
|
|
247
247
|
this.stopRow = place.row === -1 ? 'head' : this.options.rows()[place.row];
|
|
248
248
|
this.stopColumn = place.column;
|
|
249
249
|
this.focusWanted = focus;
|
|
250
|
+
if (focus && place.row >= 0) {
|
|
251
|
+
this.options.reveal?.(place.row);
|
|
252
|
+
}
|
|
250
253
|
this.host.requestUpdate();
|
|
251
254
|
}
|
|
252
255
|
mark() {
|
|
@@ -259,7 +262,11 @@ export class TableGridController {
|
|
|
259
262
|
column.gridCell = on;
|
|
260
263
|
column.tabStop = on && at.row === -1 && at.column === index + this.offset;
|
|
261
264
|
}
|
|
265
|
+
const drawn = this.options.drawn?.();
|
|
262
266
|
for (const [index, line] of this.options.rows().entries()) {
|
|
267
|
+
if (drawn && !drawn.has(line)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
263
270
|
const row = line;
|
|
264
271
|
if (row.isTableRow) {
|
|
265
272
|
const here = on && at.row === index;
|
|
@@ -36,6 +36,7 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
|
|
|
36
36
|
* The `heading` slot names the table: slot a heading or a line of text into
|
|
37
37
|
* it, and the table is named by its words. An `aria-label` or `aria-labelledby` on the
|
|
38
38
|
* host wins over it.
|
|
39
|
+
* A table with no name is reported once in a development build.
|
|
39
40
|
*
|
|
40
41
|
* **Selecting.** `selectable` adds a column of checkboxes.
|
|
41
42
|
*
|
|
@@ -121,9 +122,17 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
|
|
|
121
122
|
* | A letter or a word | Moves to the next row whose first cell starts with what you typed. |
|
|
122
123
|
* | Tab | Leaves the table. |
|
|
123
124
|
*
|
|
125
|
+
* **Long tables.** `windowed` draws only the rows in view, as Data Table's
|
|
126
|
+
* does, and the box of the table scrolls under headings that stick. The
|
|
127
|
+
* table still says how many rows it has with `aria-rowcount`, and each row
|
|
128
|
+
* its place with `aria-rowindex`. The keys reach every row, drawn or not, and
|
|
129
|
+
* the row holding focus stays drawn when it scrolls away. Nested rows,
|
|
130
|
+
* sorting, and selecting work as they do in a table that draws every row.
|
|
131
|
+
*
|
|
124
132
|
* **Loading more.** `load-more` sends `helix-load-more` once the end of the
|
|
125
133
|
* table comes within a screen of view, in the box that scrolls it or the
|
|
126
|
-
* page,
|
|
134
|
+
* page, or in the table's own box when it is `windowed`, once for each
|
|
135
|
+
* number of rows: add rows and it can send it again. Set
|
|
127
136
|
* `loading` while you fetch, and the table asks for nothing until it clears.
|
|
128
137
|
*
|
|
129
138
|
* When the rows did not load, set `error-text`, such as "The orders did not
|
|
@@ -158,6 +167,8 @@ declare const HelixHeadlessTable_base: (new (...args: any[]) => import("@fusion.
|
|
|
158
167
|
* @event {HelixSortedEvent} helix-sorted - Fired after the sort order changes, with the new order.
|
|
159
168
|
* @event {HelixToggleEvent} helix-toggle - Fired from a row once the rows under it are shown or hidden, with whether it is open in `detail.checked`.
|
|
160
169
|
*
|
|
170
|
+
* @csspart after - The space that stands in for the rows below the window. Only while `windowed`.
|
|
171
|
+
* @csspart before - The space that stands in for the rows above the window. Only while `windowed`.
|
|
161
172
|
* @csspart checkbox - The checkbox in the heading that selects every row, in a `selectable` table.
|
|
162
173
|
* @csspart checkbox-box - The box drawn for the checkbox's input.
|
|
163
174
|
* @csspart checkbox-control - The checkbox's input.
|
|
@@ -399,11 +410,35 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
|
|
|
399
410
|
sortedLabel: string;
|
|
400
411
|
/** What screen readers hear when the table stops being sorted. */
|
|
401
412
|
unsortedLabel: string;
|
|
413
|
+
/**
|
|
414
|
+
* Draws only the rows in view, with empty space in place of the rest, as
|
|
415
|
+
* Data Table's `windowed` does. The table's box then scrolls, at the
|
|
416
|
+
* height `--table-block-size` gives it in the styled tier, and the
|
|
417
|
+
* headings stick to its top.
|
|
418
|
+
*
|
|
419
|
+
* Off by default. Windowing costs a scroll listener, a measurement per row
|
|
420
|
+
* drawn, and a height on the table, and a table of twenty rows should pay
|
|
421
|
+
* none of them.
|
|
422
|
+
*/
|
|
423
|
+
windowed: boolean;
|
|
402
424
|
private sortState;
|
|
403
425
|
private selectedState;
|
|
404
426
|
private expandedState;
|
|
405
427
|
/** Whether any row sits under another, which makes the table a tree. */
|
|
406
428
|
private get nests();
|
|
429
|
+
/**
|
|
430
|
+
* What the table read from its rows, kept until anything asks for an
|
|
431
|
+
* update or the turn ends.
|
|
432
|
+
*
|
|
433
|
+
* **Read once rather than on every question.** A render, the assignment
|
|
434
|
+
* and the keyboard each asked for the rows in order dozens of times, and
|
|
435
|
+
* each answer walked every row: in Chromium a windowed table of 3,000 rows
|
|
436
|
+
* took 89 milliseconds to move its window, 42 of them reading the keys.
|
|
437
|
+
*/
|
|
438
|
+
private memo;
|
|
439
|
+
private remember;
|
|
440
|
+
/** @internal Forgets what was read from the rows, then asks for an update as Lit does. */
|
|
441
|
+
requestUpdate(name?: PropertyKey, oldValue?: unknown, options?: Parameters<HelixElement['requestUpdate']>[2]): void;
|
|
407
442
|
/** The keys of the rows whose rows are shown. */
|
|
408
443
|
private get open();
|
|
409
444
|
/**
|
|
@@ -424,6 +459,37 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
|
|
|
424
459
|
protected readonly aria: AriaForwardController;
|
|
425
460
|
/** The keyboard of a `grid` table, in `grid-controller.ts`. */
|
|
426
461
|
private readonly gridKeys;
|
|
462
|
+
/**
|
|
463
|
+
* The slice of a long table that is drawn, under `windowed`. The rows are
|
|
464
|
+
* the page's elements, so the window is which of them the body slot is
|
|
465
|
+
* given: a row it is not given is not laid out and not in the
|
|
466
|
+
* accessibility tree.
|
|
467
|
+
*/
|
|
468
|
+
private readonly virtualize;
|
|
469
|
+
/** How many rows are drawn in order, read when the rows are assigned. */
|
|
470
|
+
private arrangedCount;
|
|
471
|
+
/** A `windowed` table has rows while it holds any, drawn or not. */
|
|
472
|
+
protected get hasRows(): boolean;
|
|
473
|
+
/** The rows the body slot was last given, under `windowed`. */
|
|
474
|
+
private drawnRows;
|
|
475
|
+
/**
|
|
476
|
+
* The rows drawn now, each by its first cell, with its place among the
|
|
477
|
+
* rows. A row is `display: contents` and has no box, and a cell is a grid
|
|
478
|
+
* item stretched to its row, as Data Table measures its rows.
|
|
479
|
+
*/
|
|
480
|
+
private measuredRows;
|
|
481
|
+
/** The row outside the window that stays drawn: the grid's stop, or the row holding focus. */
|
|
482
|
+
private keptRow;
|
|
483
|
+
/**
|
|
484
|
+
* The rows to draw under `windowed`: the window, and the row kept for
|
|
485
|
+
* focus beside it on its side.
|
|
486
|
+
*/
|
|
487
|
+
private windowOf;
|
|
488
|
+
/**
|
|
489
|
+
* Scrolls drawn row `row` into view below the headings, and moves the
|
|
490
|
+
* window to it, before a key focuses it.
|
|
491
|
+
*/
|
|
492
|
+
private revealRow;
|
|
427
493
|
/**
|
|
428
494
|
* Moving rows by their grips, in a `reorderable` table. The rows are the
|
|
429
495
|
* page's, so a drop asks the page to move them, and the grips do nothing
|
|
@@ -459,6 +525,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
|
|
|
459
525
|
* which is what a framework holding those rows needs.
|
|
460
526
|
*/
|
|
461
527
|
private get arrangedRows();
|
|
528
|
+
/** The rows in the order they are drawn, worked out again. */
|
|
529
|
+
private arrange;
|
|
462
530
|
/**
|
|
463
531
|
* The rows of a tree in the order they are drawn: each row followed by the
|
|
464
532
|
* rows under it while it is open, at every depth, keeping the order `rows`
|
|
@@ -714,6 +782,8 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
|
|
|
714
782
|
* are asked for before a reader reaches the last. List does the same.
|
|
715
783
|
*/
|
|
716
784
|
private watchEnd;
|
|
785
|
+
/** Asks for more once a reader of a `windowed` table is within a screen of its end. */
|
|
786
|
+
private readonly nearEnd;
|
|
717
787
|
/** Whether the end of the table was in reach when last measured. */
|
|
718
788
|
private endInView;
|
|
719
789
|
/** Sends `helix-load-more`, once for each number of rows, and never while loading. */
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, anchorAt, assignSlots, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, treeArrow, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, anchorAt, assignSlots, auditName, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, treeArrow, VirtualizeController, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { nothing } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { html } from 'lit/static-html.js';
|
|
@@ -46,6 +46,7 @@ const lengthOf = (element) => textOf(element).length;
|
|
|
46
46
|
const numberOf = (value) => Number(value.replaceAll(',', ''));
|
|
47
47
|
const STATUS = new Set(['empty', 'error', 'loading']);
|
|
48
48
|
const NAMED = new Set([...STATUS, 'heading']);
|
|
49
|
+
const ESTIMATE = 40;
|
|
49
50
|
let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixElement) {
|
|
50
51
|
constructor() {
|
|
51
52
|
super(...arguments);
|
|
@@ -78,9 +79,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
78
79
|
this.selectRowLabel = 'Select {row}';
|
|
79
80
|
this.sortedLabel = '{direction, select, ascending {Sorted by {column}, ascending} descending {Sorted by {column}, descending} other {Sorted by {column}}}';
|
|
80
81
|
this.unsortedLabel = 'Not sorted';
|
|
82
|
+
this.windowed = false;
|
|
81
83
|
this.sortState = undefined;
|
|
82
84
|
this.selectedState = undefined;
|
|
83
85
|
this.expandedState = undefined;
|
|
86
|
+
this.memo = {};
|
|
84
87
|
this.headed = false;
|
|
85
88
|
this.aria = new AriaForwardController(this, {
|
|
86
89
|
label: true,
|
|
@@ -95,9 +98,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
95
98
|
},
|
|
96
99
|
columns: () => this.tableColumns.filter((column) => column.shown),
|
|
97
100
|
disclose: (at, forward) => this.disclose(at, forward),
|
|
101
|
+
drawn: () => (this.windowed ? new Set(this.drawnRows) : undefined),
|
|
98
102
|
lead: () => this.lead,
|
|
99
103
|
on: () => this.grid,
|
|
100
104
|
part: (name) => this.partNamed(name),
|
|
105
|
+
reveal: (row) => this.revealRow(row),
|
|
101
106
|
rows: () => (this.childrenKnown ? this.arrangedRows : []),
|
|
102
107
|
rowWords: () => this.arrangedRows.map((row) => textOf(row.children[0])),
|
|
103
108
|
selectAll: () => this.selectable &&
|
|
@@ -105,6 +110,15 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
105
110
|
this.requestSelected(new Set([...this.chosen, ...this.enabledKeys])),
|
|
106
111
|
selectRange: (to, from) => this.selectRange(to, from),
|
|
107
112
|
});
|
|
113
|
+
this.virtualize = new VirtualizeController(this, {
|
|
114
|
+
container: () => (this.windowed ? this.partNamed('table') : null),
|
|
115
|
+
count: () => this.arrangedCount,
|
|
116
|
+
estimate: () => ESTIMATE,
|
|
117
|
+
measured: () => this.windowed,
|
|
118
|
+
rows: () => (this.windowed ? this.measuredRows() : []),
|
|
119
|
+
});
|
|
120
|
+
this.arrangedCount = 0;
|
|
121
|
+
this.drawnRows = [];
|
|
108
122
|
this.reorder = new ReorderController(this, {
|
|
109
123
|
axis: 'block',
|
|
110
124
|
box: (key) => this.gripOf(this.rowFor(key))?.getBoundingClientRect(),
|
|
@@ -191,6 +205,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
191
205
|
this.watcher = undefined;
|
|
192
206
|
this.alignment = undefined;
|
|
193
207
|
this.aligned = '';
|
|
208
|
+
this.nearEnd = () => {
|
|
209
|
+
if (this.windowed && this.loadMore && this.virtualize.endInSight(this.arrangedCount)) {
|
|
210
|
+
this.askForMore();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
194
213
|
this.endInView = false;
|
|
195
214
|
}
|
|
196
215
|
static { this.words = 'helix-table'; }
|
|
@@ -219,7 +238,23 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
219
238
|
this.requestUpdate('sort', previous);
|
|
220
239
|
}
|
|
221
240
|
get nests() {
|
|
222
|
-
return this.childrenKnown && this.keyed.some(({ row }) => (row.parent ?? '') !== '');
|
|
241
|
+
return this.remember('nests', () => this.childrenKnown && this.keyed.some(({ row }) => (row.parent ?? '') !== ''));
|
|
242
|
+
}
|
|
243
|
+
remember(key, compute) {
|
|
244
|
+
const memo = this.memo;
|
|
245
|
+
if (Object.keys(memo).length === 0) {
|
|
246
|
+
queueMicrotask(() => {
|
|
247
|
+
if (this.memo === memo) {
|
|
248
|
+
this.memo = {};
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
memo[key] ??= compute();
|
|
253
|
+
return memo[key];
|
|
254
|
+
}
|
|
255
|
+
requestUpdate(name, oldValue, options) {
|
|
256
|
+
this.memo = {};
|
|
257
|
+
super.requestUpdate(name, oldValue, options);
|
|
223
258
|
}
|
|
224
259
|
get open() {
|
|
225
260
|
return new Set(commaList(this.expanded));
|
|
@@ -251,6 +286,44 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
251
286
|
row.dispatchEvent(new HelixToggleEvent({ checked: next }));
|
|
252
287
|
}
|
|
253
288
|
}
|
|
289
|
+
get hasRows() {
|
|
290
|
+
return (this.windowed && this.arrangedCount > 0) || super.hasRows;
|
|
291
|
+
}
|
|
292
|
+
measuredRows() {
|
|
293
|
+
const rows = this.arrangedRows;
|
|
294
|
+
const at = new Map(rows.map((row, index) => [row, index]));
|
|
295
|
+
return this.drawnRows.flatMap((row) => {
|
|
296
|
+
const cell = [...row.children].find((child) => child.getAttribute('hidden') === null && child.getAttribute('slot') === null);
|
|
297
|
+
const index = at.get(row);
|
|
298
|
+
return cell instanceof HTMLElement && index !== undefined ? [{ element: cell, index }] : [];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
keptRow(rows) {
|
|
302
|
+
const { first, last } = this.virtualize.window;
|
|
303
|
+
const active = this.getRootNode().activeElement;
|
|
304
|
+
const focused = this.grid
|
|
305
|
+
? this.gridKeys.current().row
|
|
306
|
+
: rows.findIndex((row) => active !== null && row.contains(active));
|
|
307
|
+
return focused >= 0 && (focused < first || focused >= last) ? focused : undefined;
|
|
308
|
+
}
|
|
309
|
+
windowOf(rows) {
|
|
310
|
+
const { first, last } = this.virtualize.window;
|
|
311
|
+
const drawn = rows.slice(first, last);
|
|
312
|
+
const kept = this.keptRow(rows);
|
|
313
|
+
const row = kept === undefined ? undefined : rows[kept];
|
|
314
|
+
if (kept !== undefined && row) {
|
|
315
|
+
return kept < first ? [row, ...drawn] : [...drawn, row];
|
|
316
|
+
}
|
|
317
|
+
return drawn;
|
|
318
|
+
}
|
|
319
|
+
revealRow(row) {
|
|
320
|
+
if (!this.windowed) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const heading = this.gridKeys.cellAt({ column: 0, row: -1 });
|
|
324
|
+
this.virtualize.reveal(row, heading?.getBoundingClientRect().height ?? 0);
|
|
325
|
+
this.virtualize.measure();
|
|
326
|
+
}
|
|
254
327
|
get lead() {
|
|
255
328
|
return [
|
|
256
329
|
...(this.reorderable ? ['grip'] : []),
|
|
@@ -282,6 +355,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
282
355
|
return this.requestSort(format(cycle(parse(this.sort), field)));
|
|
283
356
|
}
|
|
284
357
|
get arrangedRows() {
|
|
358
|
+
return this.remember('arranged', () => this.arrange());
|
|
359
|
+
}
|
|
360
|
+
arrange() {
|
|
285
361
|
const rows = this.slotted;
|
|
286
362
|
const spec = parse(this.sort);
|
|
287
363
|
const declared = this.tableColumns;
|
|
@@ -381,10 +457,10 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
381
457
|
});
|
|
382
458
|
}
|
|
383
459
|
get keyed() {
|
|
384
|
-
return this.slotted.map((row, index) => ({
|
|
460
|
+
return this.remember('keyed', () => this.slotted.map((row, index) => ({
|
|
385
461
|
key: row.value || String(index),
|
|
386
462
|
row: row,
|
|
387
|
-
}));
|
|
463
|
+
})));
|
|
388
464
|
}
|
|
389
465
|
get chosen() {
|
|
390
466
|
return new Set(commaList(this.selected));
|
|
@@ -538,10 +614,26 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
538
614
|
const tree = this.nests;
|
|
539
615
|
const shapes = tree ? this.shape() : undefined;
|
|
540
616
|
const open = this.open;
|
|
617
|
+
const drawn = this.windowed ? new Set(this.drawnRows) : undefined;
|
|
618
|
+
const places = drawn
|
|
619
|
+
? new Map(this.arrangedRows.map((row, index) => [row, index + 2]))
|
|
620
|
+
: undefined;
|
|
541
621
|
for (const { key, row } of this.keyed) {
|
|
542
622
|
if (!row.isTableRow) {
|
|
543
623
|
continue;
|
|
544
624
|
}
|
|
625
|
+
if (drawn && !drawn.has(row)) {
|
|
626
|
+
const shape = shapes?.get(key);
|
|
627
|
+
whenHydrated(row, () => {
|
|
628
|
+
row.selectable = this.selectable;
|
|
629
|
+
row.gripLabel = this.reorderable ? row.gripLabel || ' ' : '';
|
|
630
|
+
row.level = shape?.level ?? 0;
|
|
631
|
+
row.branch = shape?.branch ?? false;
|
|
632
|
+
row.open = open.has(key);
|
|
633
|
+
});
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const place = places?.get(row) ?? 0;
|
|
545
637
|
const name = textOf(row.children[0]) || key;
|
|
546
638
|
const label = formatWords(this, 'selectRowLabel', { row: name });
|
|
547
639
|
const grip = this.reorderable ? formatWords(this, 'moveRowLabel', { row: name }) : '';
|
|
@@ -560,6 +652,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
560
652
|
row.open = open.has(key);
|
|
561
653
|
row.expandLabel = expand;
|
|
562
654
|
row.expandWith = tree ? () => this.toggleRow(key) : undefined;
|
|
655
|
+
row.rowIndex = place;
|
|
563
656
|
});
|
|
564
657
|
}
|
|
565
658
|
}
|
|
@@ -676,7 +769,11 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
676
769
|
const columns = new Set(this.tableColumns);
|
|
677
770
|
const headings = new Set(this.headings);
|
|
678
771
|
const status = [...(this.children ?? [])].filter((child) => STATUS.has(child.getAttribute('slot') ?? ''));
|
|
679
|
-
|
|
772
|
+
const arranged = this.arrangedRows;
|
|
773
|
+
this.arrangedCount = arranged.length;
|
|
774
|
+
const rows = this.windowed ? this.windowOf(arranged) : arranged;
|
|
775
|
+
this.drawnRows = this.windowed ? rows : [];
|
|
776
|
+
assignSlots(this.renderRoot, [...columns, ...headings, ...status, ...rows], (child) => columns.has(child)
|
|
680
777
|
? 'columns'
|
|
681
778
|
: headings.has(child)
|
|
682
779
|
? 'heading'
|
|
@@ -719,14 +816,22 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
719
816
|
this.markSorted();
|
|
720
817
|
this.markSelected();
|
|
721
818
|
this.nameByHeading();
|
|
819
|
+
auditName(this, () => null, 'Give it a `label`, or put a heading in its `heading` slot, that says what the rows are.', () => this.label !== '' || this.headings.length > 0);
|
|
722
820
|
if (this.childrenKnown) {
|
|
723
821
|
this.gridKeys.mark();
|
|
724
822
|
this.gridKeys.place();
|
|
725
823
|
}
|
|
824
|
+
if (this.windowed) {
|
|
825
|
+
this.virtualize.measure();
|
|
826
|
+
this.virtualize.settle();
|
|
827
|
+
}
|
|
726
828
|
this.watchEnd();
|
|
727
829
|
}
|
|
728
830
|
watchEnd() {
|
|
729
|
-
|
|
831
|
+
if (this.windowed) {
|
|
832
|
+
this.nearEnd();
|
|
833
|
+
}
|
|
834
|
+
const end = this.loadMore && !this.windowed ? this.renderRoot.querySelector('[data-end]') : null;
|
|
730
835
|
if (end === this.watchedEnd) {
|
|
731
836
|
if (this.endInView) {
|
|
732
837
|
this.askForMore();
|
|
@@ -810,6 +915,9 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
810
915
|
render() {
|
|
811
916
|
const template = this.template;
|
|
812
917
|
const grid = this.grid;
|
|
918
|
+
const windowed = this.windowed && this.childrenKnown;
|
|
919
|
+
const count = windowed ? this.arrangedRows.length : 0;
|
|
920
|
+
const room = windowed ? this.virtualize.spacers(this.keptRow(this.arrangedRows)) : undefined;
|
|
813
921
|
const start = grid ? html `<span part="grid-start" tabindex="-1"></span>` : nothing;
|
|
814
922
|
const end = grid ? html `<span part="grid-end" tabindex="-1"></span>` : nothing;
|
|
815
923
|
return html `<slot name="heading"></slot>${start}<div
|
|
@@ -818,15 +926,19 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
|
|
|
818
926
|
aria-multiselectable=${grid && this.selectable ? (this.single ? 'false' : 'true') : nothing}
|
|
819
927
|
aria-busy=${this.loading ? 'true' : nothing}
|
|
820
928
|
aria-label=${this.label || this.fallbackLabel}
|
|
929
|
+
aria-rowcount=${windowed ? count + 1 : nothing}
|
|
821
930
|
style=${template ? `--table-template: ${template}` : ''}
|
|
931
|
+
@scroll=${this.nearEnd}
|
|
822
932
|
>
|
|
823
|
-
<div part="head" role="row">${this.renderGripHead()}${this.renderSelectAll()}${this.renderExpandHead()}<slot name="columns"></slot></div>
|
|
933
|
+
<div part="head" role="row" aria-rowindex=${windowed ? 1 : nothing}>${this.renderGripHead()}${this.renderSelectAll()}${this.renderExpandHead()}<slot name="columns"></slot></div>
|
|
934
|
+
${room ? html `<div part="before" role="presentation" style="block-size: ${room.before}px"></div>` : nothing}
|
|
824
935
|
<slot
|
|
825
936
|
@slotchange=${(event) => {
|
|
826
937
|
this.countRows(event);
|
|
827
938
|
this.handleSlotChange();
|
|
828
939
|
}}
|
|
829
940
|
></slot>
|
|
941
|
+
${room ? html `<div part="after" role="presentation" style="block-size: ${room.after}px"></div>` : nothing}
|
|
830
942
|
${this.errorText === ''
|
|
831
943
|
? this.renderStatus(this.hasRows, grid ? 'gridcell' : 'cell')
|
|
832
944
|
: html `<p part="error" role="row"><span role=${grid ? 'gridcell' : 'cell'}><slot name="error">${this.errorText}</slot></span></p>`}
|
|
@@ -948,6 +1060,9 @@ __decorate([
|
|
|
948
1060
|
words,
|
|
949
1061
|
property({ attribute: 'unsorted-label' })
|
|
950
1062
|
], HelixHeadlessTable.prototype, "unsortedLabel", void 0);
|
|
1063
|
+
__decorate([
|
|
1064
|
+
property({ reflect: true, type: Boolean })
|
|
1065
|
+
], HelixHeadlessTable.prototype, "windowed", void 0);
|
|
951
1066
|
__decorate([
|
|
952
1067
|
state()
|
|
953
1068
|
], HelixHeadlessTable.prototype, "sortState", void 0);
|