@fusion.dev/helix-core 0.144.0 → 0.145.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 +990 -96
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -0
- package/dist/controllers/countdown-controller/countdown-controller.d.ts +55 -0
- package/dist/controllers/countdown-controller/countdown-controller.js +57 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +11 -2
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +4 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +68 -35
- package/dist/controllers/virtualize-controller/virtualize-controller.js +77 -10
- package/dist/events/events.d.ts +186 -0
- package/dist/events/events.js +138 -0
- package/dist/helix-element/helix-element.js +14 -1
- package/dist/helix-element/react-bridge.d.ts +9 -4
- package/dist/helix-element/react-bridge.js +14 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/utils/draw-icon/draw-icon.d.ts +7 -6
- package/dist/utils/range-add/range-add.d.ts +19 -0
- package/dist/utils/range-add/range-add.js +5 -0
- package/dist/utils/reflect-elements/reflect-elements.d.ts +3 -3
- package/dist/utils/report-runtime/report-runtime.d.ts +3 -1
- package/dist/utils/write-time/write-time.d.ts +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
/** What a `CountdownController` does when its time runs out, and when it stops and starts. */
|
|
3
|
+
export interface CountdownOptions {
|
|
4
|
+
/** Runs once the time is spent. */
|
|
5
|
+
done: () => void;
|
|
6
|
+
/** Runs with `true` when the clock stops partway and `false` when it runs again. */
|
|
7
|
+
paused?: (paused: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A lifetime that pauses: a message that dismisses itself after a time, and
|
|
11
|
+
* holds that time while a reader is on it.
|
|
12
|
+
*
|
|
13
|
+
* Toast's items and Announcement each dismiss themselves after `duration`,
|
|
14
|
+
* and each stops the clock while the pointer or focus is on the message, so a
|
|
15
|
+
* reader partway through is not cut off. Resuming continues from what was
|
|
16
|
+
* left rather than starting over.
|
|
17
|
+
*
|
|
18
|
+
* **Holds are counted, not flagged.** Hover and focus stop the same clock and
|
|
19
|
+
* are independent: a pointer can leave while focus stays. Uncounted, a reader
|
|
20
|
+
* who tabs into a message and then moves the pointer off it has the clock
|
|
21
|
+
* restarted underneath them. Only the last one out starts it again.
|
|
22
|
+
*
|
|
23
|
+
* **Leaving the page pauses it**, keeping the time already spent, and drops
|
|
24
|
+
* the holds, since a pointer or focus on a detached element never leaves it.
|
|
25
|
+
* Coming back does not resume on its own: the host says whether it should,
|
|
26
|
+
* such as only while it is still showing. `DelayController` is the other wait
|
|
27
|
+
* in core, for a wait that starts over and forgets itself on leaving.
|
|
28
|
+
*/
|
|
29
|
+
export declare class CountdownController implements ReactiveController {
|
|
30
|
+
private readonly options;
|
|
31
|
+
/** When the current run began, or nothing while the clock is stopped. */
|
|
32
|
+
private startedAt;
|
|
33
|
+
/** What is left of the time, so resuming continues rather than restarts. */
|
|
34
|
+
private remaining;
|
|
35
|
+
private timer;
|
|
36
|
+
/** How many reasons there are to hold the clock. */
|
|
37
|
+
private holds;
|
|
38
|
+
constructor(host: ReactiveControllerHost, options: CountdownOptions);
|
|
39
|
+
/** Whether the clock is running now. */
|
|
40
|
+
get running(): boolean;
|
|
41
|
+
hostConnected(): void;
|
|
42
|
+
hostDisconnected(): void;
|
|
43
|
+
/** Starts the clock at `ms`. Nothing, zero or less starts nothing. */
|
|
44
|
+
start(ms: number | undefined): void;
|
|
45
|
+
/** Stops the clock and keeps the time that is left. */
|
|
46
|
+
pause(): void;
|
|
47
|
+
/** Runs the clock again from where it stopped. */
|
|
48
|
+
resume(): void;
|
|
49
|
+
/** Stops the clock where it is, as a dismissal does. */
|
|
50
|
+
cancel(): void;
|
|
51
|
+
/** One more reason to hold the clock, such as a pointer arriving. */
|
|
52
|
+
hold(): void;
|
|
53
|
+
/** One reason fewer, and the clock runs again once none is left. */
|
|
54
|
+
release(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class CountdownController {
|
|
2
|
+
constructor(host, options) {
|
|
3
|
+
this.options = options;
|
|
4
|
+
this.holds = 0;
|
|
5
|
+
host.addController(this);
|
|
6
|
+
}
|
|
7
|
+
get running() {
|
|
8
|
+
return this.timer !== undefined;
|
|
9
|
+
}
|
|
10
|
+
hostConnected() { }
|
|
11
|
+
hostDisconnected() {
|
|
12
|
+
this.pause();
|
|
13
|
+
this.holds = 0;
|
|
14
|
+
}
|
|
15
|
+
start(ms) {
|
|
16
|
+
if (ms === undefined || !(ms > 0)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.remaining = ms;
|
|
20
|
+
this.resume();
|
|
21
|
+
}
|
|
22
|
+
pause() {
|
|
23
|
+
if (this.timer === undefined || this.startedAt === undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
clearTimeout(this.timer);
|
|
27
|
+
this.timer = undefined;
|
|
28
|
+
this.remaining = Math.max((this.remaining ?? 0) - (Date.now() - this.startedAt), 0);
|
|
29
|
+
this.startedAt = undefined;
|
|
30
|
+
this.options.paused?.(true);
|
|
31
|
+
}
|
|
32
|
+
resume() {
|
|
33
|
+
if (this.timer !== undefined || this.remaining === undefined || this.remaining <= 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.startedAt = Date.now();
|
|
37
|
+
this.options.paused?.(false);
|
|
38
|
+
this.timer = setTimeout(() => {
|
|
39
|
+
this.timer = undefined;
|
|
40
|
+
this.options.done();
|
|
41
|
+
}, this.remaining);
|
|
42
|
+
}
|
|
43
|
+
cancel() {
|
|
44
|
+
clearTimeout(this.timer);
|
|
45
|
+
this.timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
hold() {
|
|
48
|
+
this.holds += 1;
|
|
49
|
+
this.pause();
|
|
50
|
+
}
|
|
51
|
+
release() {
|
|
52
|
+
this.holds = Math.max(this.holds - 1, 0);
|
|
53
|
+
if (this.holds === 0) {
|
|
54
|
+
this.resume();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -19,6 +19,8 @@ export interface NavigableItem {
|
|
|
19
19
|
/** Whether the item is the current one, which is where movement starts. */
|
|
20
20
|
selected: boolean;
|
|
21
21
|
}
|
|
22
|
+
/** Where a move starts from: the item that has focus, or the chosen item. */
|
|
23
|
+
export type KeyboardOrigin = 'focus' | 'selection';
|
|
22
24
|
/** How the controller is told what it is moving through. */
|
|
23
25
|
export interface KeyboardNavigationOptions {
|
|
24
26
|
/** The items, in the order a reader moves through them. */
|
|
@@ -35,8 +37,13 @@ export interface KeyboardNavigationOptions {
|
|
|
35
37
|
* every time. It is also the only reading that can tell a press on a tab from
|
|
36
38
|
* a press somewhere else in the component, which matters wherever the host
|
|
37
39
|
* holds more than the set (a tab panel is a tab stop of its own).
|
|
40
|
+
*
|
|
41
|
+
* A function where the answer changes with the element's state, read on every
|
|
42
|
+
* press. A read-only radio group is the case: while it is read-only the arrows
|
|
43
|
+
* move focus between radios without choosing, so the move has to start where
|
|
44
|
+
* focus is, and once it is editable again the selection is the start.
|
|
38
45
|
*/
|
|
39
|
-
origin?:
|
|
46
|
+
origin?: KeyboardOrigin | (() => KeyboardOrigin);
|
|
40
47
|
/**
|
|
41
48
|
* Whether arriving also chooses.
|
|
42
49
|
*
|
|
@@ -176,13 +183,15 @@ export declare class Typeahead {
|
|
|
176
183
|
export declare class KeyboardNavigationController implements ReactiveController {
|
|
177
184
|
private readonly host;
|
|
178
185
|
private readonly items;
|
|
179
|
-
private readonly
|
|
186
|
+
private readonly originOf;
|
|
180
187
|
private readonly selects;
|
|
181
188
|
private readonly reserved;
|
|
182
189
|
private readonly wrap;
|
|
183
190
|
private readonly words;
|
|
184
191
|
private readonly typeahead;
|
|
185
192
|
constructor(host: ReactiveControllerHost & HTMLElement, { items, origin, reserved, selects, typeahead, wrap, }: KeyboardNavigationOptions);
|
|
193
|
+
/** Where this press starts from, read again on every press. */
|
|
194
|
+
private get origin();
|
|
186
195
|
/** Moves to an item, and chooses it where arriving is choosing. */
|
|
187
196
|
private arrive;
|
|
188
197
|
/**
|
|
@@ -81,13 +81,16 @@ export class KeyboardNavigationController {
|
|
|
81
81
|
};
|
|
82
82
|
this.host = host;
|
|
83
83
|
this.items = items;
|
|
84
|
-
this.
|
|
84
|
+
this.originOf = typeof origin === 'function' ? origin : () => origin;
|
|
85
85
|
this.reserved = reserved;
|
|
86
86
|
this.selects = selects;
|
|
87
87
|
this.words = typeahead;
|
|
88
88
|
this.wrap = wrap;
|
|
89
89
|
host.addController(this);
|
|
90
90
|
}
|
|
91
|
+
get origin() {
|
|
92
|
+
return this.originOf();
|
|
93
|
+
}
|
|
91
94
|
arrive(target) {
|
|
92
95
|
target?.focusItem();
|
|
93
96
|
if (this.selects()) {
|
|
@@ -63,6 +63,16 @@ export interface Window {
|
|
|
63
63
|
/** Index one past the last item rendered. */
|
|
64
64
|
last: number;
|
|
65
65
|
}
|
|
66
|
+
/** Where `reveal` puts an item, and how it gets there. */
|
|
67
|
+
export interface RevealOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Where the item lands: the least distance that shows it whole (`nearest`,
|
|
70
|
+
* the default), or at the box's `start`, `center` or `end`.
|
|
71
|
+
*/
|
|
72
|
+
align?: 'center' | 'end' | 'nearest' | 'start';
|
|
73
|
+
/** The platform's scroll behavior. Instant when left out. */
|
|
74
|
+
behavior?: ScrollBehavior;
|
|
75
|
+
}
|
|
66
76
|
/**
|
|
67
77
|
* Works out which slice of a long set is worth rendering.
|
|
68
78
|
*
|
|
@@ -86,35 +96,14 @@ export interface Window {
|
|
|
86
96
|
* track, so the room the absent rows would have taken is left as padding before
|
|
87
97
|
* and after. A reader dragging that thumb is dragging against the real length.
|
|
88
98
|
*
|
|
89
|
-
* **
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* not
|
|
97
|
-
*
|
|
98
|
-
* **A third channel is open.** Data as *text*
|
|
99
|
-
* in the markup, a `<script type="application/json">` child or an attribute, is
|
|
100
|
-
* neither a property nor a node per row: React's hydration bug cannot touch text
|
|
101
|
-
* it renders as a child, a server emits it, and the node count is one. The
|
|
102
|
-
* charts already read data out of the document this way, `for="<id>"` at a real
|
|
103
|
-
* `<table>`, so the pattern is in the library rather than being invented for
|
|
104
|
-
* this.
|
|
105
|
-
*
|
|
106
|
-
* So the thing actually blocked is the *row template*. Turning an item into
|
|
107
|
-
* markup wants either a render function, which is a function property and dies
|
|
108
|
-
* on the same bug, or a `<template>` with named holes, which survives because it
|
|
109
|
-
* is markup and which no component here asks anyone to write. That is a decision
|
|
110
|
-
* about what this library is willing to look like, not a platform limit, and it
|
|
111
|
-
* is open at #71.
|
|
112
|
-
*
|
|
113
|
-
* So what this buys is the expensive half rather than all of it: the rows
|
|
114
|
-
* outside the window are not laid out and not painted, which is where the time
|
|
115
|
-
* goes in a long list, and the node count stays. That is worth having and it is
|
|
116
|
-
* not worth calling something it is not. An element that windows children it
|
|
117
|
-
* was handed is not a virtual list, whatever it is named.
|
|
99
|
+
* **The set arrives as data**, which is what lets a host create only the rows
|
|
100
|
+
* in the window. A host takes it through a property (an `items` array, which
|
|
101
|
+
* `applyReactProperties` carries across React hydration with every other object
|
|
102
|
+
* property) or as text in the markup (a `<script type="application/json">`
|
|
103
|
+
* named by `for`, which a server emits and which is one node whatever the
|
|
104
|
+
* row count). A host that is handed its rows as light-DOM children still gets
|
|
105
|
+
* the expensive half from this: the rows outside the window are not laid out
|
|
106
|
+
* and not painted, and the node count stays.
|
|
118
107
|
*
|
|
119
108
|
* **One estimate, not per-row measurement.** Measuring every row to place it is
|
|
120
109
|
* how a virtual list ends up reading the layout it is trying to avoid, and it
|
|
@@ -166,6 +155,39 @@ export declare class VirtualizeController implements ReactiveController {
|
|
|
166
155
|
* on screen plus the overscan, whatever the set's length.
|
|
167
156
|
*/
|
|
168
157
|
private placed;
|
|
158
|
+
/**
|
|
159
|
+
* The items on screen, with no overscan: `first` is the item at the top of
|
|
160
|
+
* the box and `last` is one past the item at its bottom, so
|
|
161
|
+
* `items.slice(first, last)` is what a reader sees.
|
|
162
|
+
*
|
|
163
|
+
* For a host that reports what is visible, where `window` is what is drawn.
|
|
164
|
+
* An empty span (`first === last`) while the box has no height or the set
|
|
165
|
+
* no items.
|
|
166
|
+
*/
|
|
167
|
+
visible(): {
|
|
168
|
+
first: number;
|
|
169
|
+
last: number;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* How much of the set lies below the bottom of the box, in pixels.
|
|
173
|
+
*
|
|
174
|
+
* For a host that asks for more items as a reader nears the end. Read from
|
|
175
|
+
* the arithmetic and not from `scrollHeight`, which also counts whatever the
|
|
176
|
+
* host draws around the rows.
|
|
177
|
+
*/
|
|
178
|
+
remaining(): number;
|
|
179
|
+
/**
|
|
180
|
+
* The two spacers' heights for the window, with item `kept` drawn too.
|
|
181
|
+
*
|
|
182
|
+
* For a host that keeps an item outside the window drawn, the row holding
|
|
183
|
+
* focus say. The item takes its own room out of the spacer on its side, so
|
|
184
|
+
* the scrollbar still measures the set. Every host that keeps a focused row
|
|
185
|
+
* asks this, so the arithmetic is written once.
|
|
186
|
+
*/
|
|
187
|
+
spacers(kept?: number): {
|
|
188
|
+
after: number;
|
|
189
|
+
before: number;
|
|
190
|
+
};
|
|
169
191
|
/**
|
|
170
192
|
* Measures what was just drawn, and holds the reader still if it moved.
|
|
171
193
|
*
|
|
@@ -206,14 +228,25 @@ export declare class VirtualizeController implements ReactiveController {
|
|
|
206
228
|
*/
|
|
207
229
|
sizeOf(index: number): number;
|
|
208
230
|
/**
|
|
209
|
-
* Scrolls the box
|
|
231
|
+
* Scrolls the box to show item `index`.
|
|
232
|
+
*
|
|
233
|
+
* By default the least distance that shows it whole, for a host moving focus
|
|
234
|
+
* to an item outside the window, whose own element cannot be scrolled to
|
|
235
|
+
* because it is not drawn where it belongs. `inset` is how far into the box
|
|
236
|
+
* the items start, for a heading that sticks at its top and covers that much
|
|
237
|
+
* of every item scrolled under it.
|
|
210
238
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
239
|
+
* `align` puts the item at the box's `start`, `center` or `end` instead, and
|
|
240
|
+
* `behavior` is the platform's, for a host whose consumer asked to be taken
|
|
241
|
+
* to an item. A smooth scroll moves the window through the box's own
|
|
242
|
+
* `scroll` events, the same as a reader scrolling.
|
|
243
|
+
*/
|
|
244
|
+
reveal(index: number, inset?: number, { align, behavior }?: RevealOptions): void;
|
|
245
|
+
/**
|
|
246
|
+
* Where item `index` starts along the scroll axis, in pixels from the top of
|
|
247
|
+
* the set: what was measured above it, or the estimate.
|
|
215
248
|
*/
|
|
216
|
-
|
|
249
|
+
offsetOf(index: number): number;
|
|
217
250
|
/**
|
|
218
251
|
* Drops every measurement, for a host that swapped its data.
|
|
219
252
|
*
|
|
@@ -39,6 +39,7 @@ export class VirtualizeController {
|
|
|
39
39
|
}
|
|
40
40
|
this.observer.observe(box);
|
|
41
41
|
box.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
42
|
+
box.setAttribute('data-virtual', '');
|
|
42
43
|
this.watching = true;
|
|
43
44
|
}
|
|
44
45
|
measure() {
|
|
@@ -97,6 +98,48 @@ export class VirtualizeController {
|
|
|
97
98
|
last,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
101
|
+
visible() {
|
|
102
|
+
const box = this.options.container();
|
|
103
|
+
const total = Math.max(0, this.options.count());
|
|
104
|
+
if (!box || total === 0) {
|
|
105
|
+
return { first: 0, last: 0 };
|
|
106
|
+
}
|
|
107
|
+
const top = Math.max(0, box.scrollTop);
|
|
108
|
+
const bottom = top + box.clientHeight;
|
|
109
|
+
if (!this.measuring()) {
|
|
110
|
+
const size = Math.max(1, this.options.estimate());
|
|
111
|
+
const first = Math.min(Math.floor(top / size), total);
|
|
112
|
+
return { first, last: Math.max(first, Math.min(total, Math.ceil(bottom / size))) };
|
|
113
|
+
}
|
|
114
|
+
const first = this.heights.indexAt(top, total);
|
|
115
|
+
let last = first;
|
|
116
|
+
while (last < total && this.heights.before(last, total) < bottom) {
|
|
117
|
+
last += 1;
|
|
118
|
+
}
|
|
119
|
+
return { first, last };
|
|
120
|
+
}
|
|
121
|
+
remaining() {
|
|
122
|
+
const box = this.options.container();
|
|
123
|
+
const total = Math.max(0, this.options.count());
|
|
124
|
+
if (!box) {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
const length = this.measuring()
|
|
128
|
+
? this.heights.total(total)
|
|
129
|
+
: total * Math.max(1, this.options.estimate());
|
|
130
|
+
return Math.max(0, length - Math.max(0, box.scrollTop) - box.clientHeight);
|
|
131
|
+
}
|
|
132
|
+
spacers(kept) {
|
|
133
|
+
const { after, before, first, last } = this.window;
|
|
134
|
+
if (kept === undefined || (kept >= first && kept < last)) {
|
|
135
|
+
return { after, before };
|
|
136
|
+
}
|
|
137
|
+
const room = this.sizeOf(kept);
|
|
138
|
+
return {
|
|
139
|
+
after: kept >= last ? Math.max(0, after - room) : after,
|
|
140
|
+
before: kept < first ? Math.max(0, before - room) : before,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
100
143
|
hostUpdated() {
|
|
101
144
|
if (this.owed !== 0) {
|
|
102
145
|
const box = this.options.container();
|
|
@@ -149,22 +192,46 @@ export class VirtualizeController {
|
|
|
149
192
|
sizeOf(index) {
|
|
150
193
|
return this.measuring() ? this.heights.at(index) : Math.max(1, this.options.estimate());
|
|
151
194
|
}
|
|
152
|
-
reveal(index, inset = 0) {
|
|
195
|
+
reveal(index, inset = 0, { align = 'nearest', behavior } = {}) {
|
|
153
196
|
const box = this.options.container();
|
|
154
197
|
if (!box) {
|
|
155
198
|
return;
|
|
156
199
|
}
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
200
|
+
const top = this.offsetOf(index);
|
|
201
|
+
const size = this.sizeOf(index);
|
|
202
|
+
const room = box.clientHeight - inset;
|
|
203
|
+
let to;
|
|
204
|
+
if (align === 'start') {
|
|
205
|
+
to = top;
|
|
206
|
+
}
|
|
207
|
+
else if (align === 'center') {
|
|
208
|
+
to = top - inset - (room - size) / 2;
|
|
209
|
+
}
|
|
210
|
+
else if (align === 'end') {
|
|
211
|
+
to = top + size + inset - box.clientHeight;
|
|
212
|
+
}
|
|
213
|
+
else if (top < box.scrollTop) {
|
|
214
|
+
to = top;
|
|
164
215
|
}
|
|
165
|
-
else if (
|
|
166
|
-
|
|
216
|
+
else if (top + size + inset > box.scrollTop + box.clientHeight) {
|
|
217
|
+
to = top + size + inset - box.clientHeight;
|
|
167
218
|
}
|
|
219
|
+
else {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const next = Math.max(0, to);
|
|
223
|
+
if (behavior && typeof box.scrollTo === 'function') {
|
|
224
|
+
box.scrollTo({ behavior, top: next });
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
box.scrollTop = next;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
offsetOf(index) {
|
|
231
|
+
const total = Math.max(0, this.options.count());
|
|
232
|
+
return this.measuring()
|
|
233
|
+
? this.heights.before(index, total)
|
|
234
|
+
: Math.max(0, Math.min(index, total)) * Math.max(1, this.options.estimate());
|
|
168
235
|
}
|
|
169
236
|
forget() {
|
|
170
237
|
this.heights.forget();
|