@fusion.dev/helix-core 0.182.1 → 0.183.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 +1677 -349
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +4 -0
- package/dist/controllers/virtualize-controller/heights.d.ts +7 -0
- package/dist/controllers/virtualize-controller/heights.js +7 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +24 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +40 -20
- package/dist/controllers/virtualize-controller/waterfall.d.ts +66 -0
- package/dist/controllers/virtualize-controller/waterfall.js +137 -0
- package/dist/helix-element/helix-element.d.ts +11 -1
- package/dist/helix-element/helix-element.js +7 -6
- package/dist/helix-element/mark-rules.d.ts +119 -0
- package/dist/helix-element/mark-rules.js +4 -0
- package/dist/helix-form-element/helix-form-element-base.d.ts +264 -0
- package/dist/helix-form-element/helix-form-element-base.js +173 -0
- package/dist/helix-form-element/helix-form-element.d.ts +8 -226
- package/dist/helix-form-element/helix-form-element.js +2 -157
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +1 -6
- package/dist/mixins/mixin-chart-status/mixin-chart-status.js +1 -6
- package/dist/mixins/mixin-collection/mixin-collection.js +1 -6
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +1 -6
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +1 -6
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +1 -6
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +1 -6
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +1 -6
- package/dist/mixins/mixin-field/mixin-field.js +1 -6
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +1 -6
- package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.js +1 -6
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +1 -6
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +1 -6
- package/dist/mixins/mixin-selectable-item/mixin-selectable-item.js +1 -6
- package/dist/mixins/mixin-tokens/mixin-tokens.js +1 -6
- package/dist/mixins/mixin-viewport/mixin-viewport.js +1 -6
- package/dist/utils/audit-name/audit-name.d.ts +3 -3
- package/dist/utils/audit-name/audit-name.js +7 -2
- package/dist/utils/implicit-submit/implicit-submit.js +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
|
@@ -35,6 +35,10 @@ export interface AriaForwardOptions {
|
|
|
35
35
|
* How to name the control, said in a warning when it has no accessible
|
|
36
36
|
* name once it has rendered. Absent, the name is not checked.
|
|
37
37
|
*
|
|
38
|
+
* A sentence, or the key of one `audit-name.ts` keeps, as `'button'` is. A
|
|
39
|
+
* key keeps the sentence out of a production build, where the check does
|
|
40
|
+
* nothing.
|
|
41
|
+
*
|
|
38
42
|
* For a control named by its content or a host attribute, as a button is. A
|
|
39
43
|
* field is checked by `MixinField`, which knows its label.
|
|
40
44
|
*/
|
|
@@ -29,6 +29,8 @@ export declare class Heights {
|
|
|
29
29
|
private dirty;
|
|
30
30
|
/** The estimate the sums were built against, so a change to it rebuilds. */
|
|
31
31
|
private built;
|
|
32
|
+
/** Counts each change to a height, for a placement built from them. */
|
|
33
|
+
private changes;
|
|
32
34
|
constructor(estimate: () => number);
|
|
33
35
|
/** How tall one item is: what was measured, or what is assumed. */
|
|
34
36
|
at(index: number): number;
|
|
@@ -42,6 +44,11 @@ export declare class Heights {
|
|
|
42
44
|
record(index: number, size: number): boolean;
|
|
43
45
|
/** The room items `[0, index)` take. */
|
|
44
46
|
before(index: number, count: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Says which heights these are: a placement built from a stamp it was
|
|
49
|
+
* given again holds, and one built from another stamp is out of date.
|
|
50
|
+
*/
|
|
51
|
+
stamp(): string;
|
|
45
52
|
/** The room the whole set takes. */
|
|
46
53
|
total(count: number): number;
|
|
47
54
|
/**
|
|
@@ -4,6 +4,7 @@ export class Heights {
|
|
|
4
4
|
this.sums = [0];
|
|
5
5
|
this.dirty = 0;
|
|
6
6
|
this.built = 0;
|
|
7
|
+
this.changes = 0;
|
|
7
8
|
this.estimate = estimate;
|
|
8
9
|
}
|
|
9
10
|
at(index) {
|
|
@@ -23,12 +24,16 @@ export class Heights {
|
|
|
23
24
|
return false;
|
|
24
25
|
}
|
|
25
26
|
this.dirty = Math.min(this.dirty, index);
|
|
27
|
+
this.changes += 1;
|
|
26
28
|
return true;
|
|
27
29
|
}
|
|
28
30
|
before(index, count) {
|
|
29
31
|
this.build(count);
|
|
30
32
|
return this.sums[Math.max(0, Math.min(index, count))] ?? 0;
|
|
31
33
|
}
|
|
34
|
+
stamp() {
|
|
35
|
+
return `${this.changes}:${this.estimate()}`;
|
|
36
|
+
}
|
|
32
37
|
total(count) {
|
|
33
38
|
return this.before(count, count);
|
|
34
39
|
}
|
|
@@ -63,11 +68,13 @@ export class Heights {
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
this.dirty = 0;
|
|
71
|
+
this.changes += 1;
|
|
66
72
|
}
|
|
67
73
|
forget() {
|
|
68
74
|
this.seen.clear();
|
|
69
75
|
this.dirty = 0;
|
|
70
76
|
this.sums = [0];
|
|
77
|
+
this.changes += 1;
|
|
71
78
|
}
|
|
72
79
|
build(count) {
|
|
73
80
|
const size = Math.max(1, this.estimate());
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import type { Waterfall } from './waterfall.js';
|
|
2
3
|
/** What the controller needs of a set and the box it is shown in. */
|
|
3
4
|
export interface VirtualizeOptions {
|
|
4
5
|
/** The scrolling box, or null before it renders. */
|
|
@@ -75,6 +76,16 @@ export interface VirtualizeOptions {
|
|
|
75
76
|
* scroll offset during a fling.
|
|
76
77
|
*/
|
|
77
78
|
anchor?: () => boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Lays the items out down several columns, each item under the column that
|
|
81
|
+
* ends highest, as a wall of photos is: a `Waterfall`, whose `lanes` says how
|
|
82
|
+
* many columns. The window is then every item that reaches into the box, in
|
|
83
|
+
* the order of the set, and a host draws each item at its column,
|
|
84
|
+
* `waterfall.lane(index)`, and at `offsetOf(index)` down the set, which
|
|
85
|
+
* `offsetOf(count)` is the length of. Items are placed by measurement, so a
|
|
86
|
+
* host supplies `rows`. None by default.
|
|
87
|
+
*/
|
|
88
|
+
waterfall?: () => Waterfall | undefined;
|
|
78
89
|
}
|
|
79
90
|
/** A row that has been drawn, and which item of the set it drew. */
|
|
80
91
|
export interface Rendered {
|
|
@@ -320,6 +331,19 @@ export declare class VirtualizeController implements ReactiveController {
|
|
|
320
331
|
private sampleRows;
|
|
321
332
|
/** Whether this pass places by measurement: offered, and wanted or needed. */
|
|
322
333
|
private measuring;
|
|
334
|
+
/** The waterfall the host lays its items out in, placed by the heights known now. */
|
|
335
|
+
private laned;
|
|
336
|
+
/**
|
|
337
|
+
* Where item `index` starts down the set, before any drift: after the items
|
|
338
|
+
* before it, or in its column of a waterfall, where the starts never go back
|
|
339
|
+
* up either.
|
|
340
|
+
*/
|
|
341
|
+
private startOf;
|
|
342
|
+
/**
|
|
343
|
+
* The item at `offset`: the last to start at or before it, or in a
|
|
344
|
+
* waterfall the first to reach past it.
|
|
345
|
+
*/
|
|
346
|
+
private indexAt;
|
|
323
347
|
/**
|
|
324
348
|
* The measured version, which asks where each item starts rather than dividing.
|
|
325
349
|
*
|
|
@@ -159,14 +159,13 @@ export class VirtualizeController {
|
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
const overscan = this.options.overscan ?? 4;
|
|
162
|
-
|
|
162
|
+
const choose = () => this.measuring()
|
|
163
163
|
? this.placed(box, total, overscan)
|
|
164
164
|
: this.uniform(box, total, size, overscan);
|
|
165
|
+
let next = choose();
|
|
165
166
|
if (this.drift !== 0 && (next.before < 0 || next.first === 0)) {
|
|
166
167
|
this.square();
|
|
167
|
-
next =
|
|
168
|
-
? this.placed(box, total, overscan)
|
|
169
|
-
: this.uniform(box, total, size, overscan);
|
|
168
|
+
next = choose();
|
|
170
169
|
}
|
|
171
170
|
if (this.last?.count !== total || this.last.estimate !== size) {
|
|
172
171
|
this.last = { count: total, estimate: size };
|
|
@@ -235,22 +234,40 @@ export class VirtualizeController {
|
|
|
235
234
|
}
|
|
236
235
|
}
|
|
237
236
|
measuring() {
|
|
238
|
-
return this.options.rows !== undefined &&
|
|
237
|
+
return (this.options.rows !== undefined &&
|
|
238
|
+
(this.uneven ||
|
|
239
|
+
this.options.waterfall?.() !== undefined ||
|
|
240
|
+
(this.options.measured?.() ?? true)));
|
|
241
|
+
}
|
|
242
|
+
laned(total) {
|
|
243
|
+
const lanes = this.options.rows ? this.options.waterfall?.() : undefined;
|
|
244
|
+
lanes?.place(total, (index) => this.heights.at(index), this.heights.stamp());
|
|
245
|
+
return lanes;
|
|
246
|
+
}
|
|
247
|
+
startOf(index, total) {
|
|
248
|
+
return this.laned(total)?.start(index) ?? this.heights.before(index, total);
|
|
249
|
+
}
|
|
250
|
+
indexAt(offset, total) {
|
|
251
|
+
const lanes = this.laned(total);
|
|
252
|
+
return lanes
|
|
253
|
+
? Math.min(lanes.from(offset), Math.max(0, total - 1))
|
|
254
|
+
: this.heights.indexAt(offset, total);
|
|
239
255
|
}
|
|
240
256
|
placed(box, total, overscan) {
|
|
241
257
|
const top = this.offset(box);
|
|
242
258
|
const lead = this.lead(box);
|
|
243
259
|
const from = this.heading < 0 ? top - lead : top;
|
|
244
|
-
const
|
|
260
|
+
const more = overscan * (this.laned(total)?.lanes ?? 1);
|
|
261
|
+
const first = Math.max(0, this.indexAt(from, total) - more);
|
|
245
262
|
const bottom = top + this.span(box) + (this.heading > 0 ? lead : 0);
|
|
246
|
-
let last = this.
|
|
247
|
-
while (last < total && this.
|
|
263
|
+
let last = this.indexAt(top, total);
|
|
264
|
+
while (last < total && this.startOf(last, total) < bottom) {
|
|
248
265
|
last += 1;
|
|
249
266
|
}
|
|
250
|
-
last = Math.min(total, last +
|
|
267
|
+
last = Math.min(total, last + more);
|
|
251
268
|
return {
|
|
252
|
-
after: Math.max(0, this.
|
|
253
|
-
before: this.
|
|
269
|
+
after: Math.max(0, this.startOf(total, total) - this.startOf(last, total)),
|
|
270
|
+
before: this.startOf(first, total) + this.drift,
|
|
254
271
|
first,
|
|
255
272
|
last,
|
|
256
273
|
};
|
|
@@ -268,20 +285,20 @@ export class VirtualizeController {
|
|
|
268
285
|
const first = Math.min(Math.floor(top / size), total);
|
|
269
286
|
return { first, last: Math.max(first, Math.min(total, Math.ceil(bottom / size))) };
|
|
270
287
|
}
|
|
271
|
-
const first = this.
|
|
288
|
+
const first = this.indexAt(top, total);
|
|
272
289
|
let last = first;
|
|
273
|
-
while (last < total && this.
|
|
290
|
+
while (last < total && this.startOf(last, total) < bottom) {
|
|
274
291
|
last += 1;
|
|
275
292
|
}
|
|
276
293
|
return { first, last };
|
|
277
294
|
}
|
|
278
295
|
remaining() {
|
|
279
296
|
const box = this.options.container();
|
|
280
|
-
const total = Math.max(0, this.options.count());
|
|
281
297
|
if (!box) {
|
|
282
298
|
return 0;
|
|
283
299
|
}
|
|
284
|
-
const
|
|
300
|
+
const total = Math.max(0, this.options.count());
|
|
301
|
+
const length = this.measuring() ? this.startOf(total, total) : total * this.line();
|
|
285
302
|
return Math.max(0, length - Math.max(0, this.offset(box)) - this.span(box));
|
|
286
303
|
}
|
|
287
304
|
endInSight(length = Math.max(0, this.options.count())) {
|
|
@@ -332,8 +349,8 @@ export class VirtualizeController {
|
|
|
332
349
|
const box = this.options.container();
|
|
333
350
|
const from = box ? this.noteClamp(box) : this.lastTop;
|
|
334
351
|
const top = box ? this.offset(box) : 0;
|
|
335
|
-
const anchor = this.target ?? this.
|
|
336
|
-
const was = this.
|
|
352
|
+
const anchor = this.target ?? this.indexAt(top, total);
|
|
353
|
+
const was = this.startOf(anchor, total);
|
|
337
354
|
let news = false;
|
|
338
355
|
for (const { element, index } of rows) {
|
|
339
356
|
if (this.heights.record(index, this.length(element) + this.gap())) {
|
|
@@ -344,7 +361,7 @@ export class VirtualizeController {
|
|
|
344
361
|
return;
|
|
345
362
|
}
|
|
346
363
|
this.settling = 0;
|
|
347
|
-
const moved = this.
|
|
364
|
+
const moved = this.startOf(anchor, total) - was;
|
|
348
365
|
if (box && top > 0 && moved !== 0 && (this.options.anchor?.() ?? true)) {
|
|
349
366
|
if (this.scrolling()) {
|
|
350
367
|
this.drift -= moved;
|
|
@@ -353,6 +370,9 @@ export class VirtualizeController {
|
|
|
353
370
|
this.scrollBox(box, from + moved);
|
|
354
371
|
}
|
|
355
372
|
}
|
|
373
|
+
if (this.laned(total)) {
|
|
374
|
+
this.host.requestUpdate();
|
|
375
|
+
}
|
|
356
376
|
this.measure();
|
|
357
377
|
}
|
|
358
378
|
shift(by) {
|
|
@@ -361,7 +381,7 @@ export class VirtualizeController {
|
|
|
361
381
|
}
|
|
362
382
|
const count = Math.max(0, this.options.count());
|
|
363
383
|
this.heights.shift(by);
|
|
364
|
-
const room = this.
|
|
384
|
+
const room = this.startOf(by, count);
|
|
365
385
|
this.window = {
|
|
366
386
|
...this.window,
|
|
367
387
|
before: this.window.before + room,
|
|
@@ -416,7 +436,7 @@ export class VirtualizeController {
|
|
|
416
436
|
offsetOf(index) {
|
|
417
437
|
const total = Math.max(0, this.options.count());
|
|
418
438
|
return this.measuring()
|
|
419
|
-
? this.
|
|
439
|
+
? this.startOf(index, total) + this.drift
|
|
420
440
|
: Math.max(0, Math.min(index, total)) * this.line();
|
|
421
441
|
}
|
|
422
442
|
forget() {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each item of a waterfall stands: a set laid out down several columns
|
|
3
|
+
* of one width, each item under the shortest column so far, as a masonry wall
|
|
4
|
+
* of photos is.
|
|
5
|
+
*
|
|
6
|
+
* **Its own module, which a host hands to `VirtualizeController`**, so the
|
|
7
|
+
* hosts that window a single column carry none of it. The controller asks it
|
|
8
|
+
* which items reach the box and where each one starts, and the host draws each
|
|
9
|
+
* item at its column and its start.
|
|
10
|
+
*
|
|
11
|
+
* **Placed in the order of the set, each item under the column that ends
|
|
12
|
+
* highest**, the leftmost of a tie, as React Aria's `WaterfallLayout` and
|
|
13
|
+
* TanStack Virtual's lanes place them. So an item's start is never above the
|
|
14
|
+
* start of the item before it, and the first item to reach any offset is found
|
|
15
|
+
* by a binary search, as one column finds it.
|
|
16
|
+
*
|
|
17
|
+
* Sizes are the controller's: what was measured, or the estimate, each with
|
|
18
|
+
* the gap below it.
|
|
19
|
+
*/
|
|
20
|
+
export declare class Waterfall {
|
|
21
|
+
/** How many columns the items are laid out in, at least one. */
|
|
22
|
+
lanes: number;
|
|
23
|
+
/** Where each item starts, in pixels from the top of the set. */
|
|
24
|
+
private starts;
|
|
25
|
+
/** The column each item stands in. */
|
|
26
|
+
private columns;
|
|
27
|
+
/** How much room each item takes, with the gap below it. */
|
|
28
|
+
private sizes;
|
|
29
|
+
/** How far down the set the items `[0, i]` reach, the furthest of their ends. */
|
|
30
|
+
private reach;
|
|
31
|
+
/** How long the set is: where its longest column ends. */
|
|
32
|
+
private extent;
|
|
33
|
+
/** What the placement was built from, so it is built again only when that changes. */
|
|
34
|
+
private built;
|
|
35
|
+
/**
|
|
36
|
+
* Places `count` items whose sizes `size` gives, unless `stamp` says the
|
|
37
|
+
* sizes, the count and the columns are the ones already placed.
|
|
38
|
+
*/
|
|
39
|
+
place(count: number, size: (index: number) => number, stamp: string): void;
|
|
40
|
+
/** Where item `index` starts, and where the set ends for an index past it. */
|
|
41
|
+
start(index: number): number;
|
|
42
|
+
/** The column item `index` stands in. */
|
|
43
|
+
lane(index: number): number;
|
|
44
|
+
/** How long the set is, where its longest column ends. */
|
|
45
|
+
length(): number;
|
|
46
|
+
/** The first item that reaches past `offset`, or the count where none does. */
|
|
47
|
+
from(offset: number): number;
|
|
48
|
+
/** The first item that starts at or past `offset`, so every item before it starts above. */
|
|
49
|
+
to(offset: number): number;
|
|
50
|
+
/** The first index whose number in `sorted` is past `offset`. */
|
|
51
|
+
private search;
|
|
52
|
+
/**
|
|
53
|
+
* Which item a key moves to from `at`, in a set of `count` items, or
|
|
54
|
+
* `undefined` for a key it does not take or where there is no item that way:
|
|
55
|
+
*
|
|
56
|
+
* - Down and Up move to the next or the previous item in the same column.
|
|
57
|
+
* - `next` and `previous` move to the column beside, to its item that sits
|
|
58
|
+
* beside this one the most, or the nearest above where none does. Which
|
|
59
|
+
* of Left and Right is `next` is the caller's, for a right-to-left page.
|
|
60
|
+
* - Page Down and Page Up move down or up the same column by `page` pixels,
|
|
61
|
+
* to the item there, or the last item that way.
|
|
62
|
+
*/
|
|
63
|
+
step(key: string, at: number, count: number, page: number): number | undefined;
|
|
64
|
+
/** How much room item `index` takes down its column, with the gap below it. */
|
|
65
|
+
private size;
|
|
66
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export class Waterfall {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.lanes = 1;
|
|
4
|
+
this.starts = [];
|
|
5
|
+
this.columns = [];
|
|
6
|
+
this.sizes = [];
|
|
7
|
+
this.reach = [];
|
|
8
|
+
this.extent = 0;
|
|
9
|
+
this.built = '';
|
|
10
|
+
}
|
|
11
|
+
place(count, size, stamp) {
|
|
12
|
+
const lanes = Math.max(1, Math.trunc(this.lanes) || 1);
|
|
13
|
+
const key = `${count}:${lanes}:${stamp}`;
|
|
14
|
+
if (key === this.built) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.built = key;
|
|
18
|
+
const ends = new Array(lanes).fill(0);
|
|
19
|
+
this.starts = new Array(count);
|
|
20
|
+
this.columns = new Array(count);
|
|
21
|
+
this.sizes = new Array(count);
|
|
22
|
+
this.reach = new Array(count);
|
|
23
|
+
let furthest = 0;
|
|
24
|
+
for (let index = 0; index < count; index++) {
|
|
25
|
+
let lane = 0;
|
|
26
|
+
for (let other = 1; other < lanes; other++) {
|
|
27
|
+
if ((ends[other] ?? 0) < (ends[lane] ?? 0)) {
|
|
28
|
+
lane = other;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const start = ends[lane] ?? 0;
|
|
32
|
+
const room = size(index);
|
|
33
|
+
const end = start + room;
|
|
34
|
+
ends[lane] = end;
|
|
35
|
+
furthest = Math.max(furthest, end);
|
|
36
|
+
this.starts[index] = start;
|
|
37
|
+
this.columns[index] = lane;
|
|
38
|
+
this.sizes[index] = room;
|
|
39
|
+
this.reach[index] = furthest;
|
|
40
|
+
}
|
|
41
|
+
this.extent = furthest;
|
|
42
|
+
}
|
|
43
|
+
start(index) {
|
|
44
|
+
return this.starts[index] ?? this.extent;
|
|
45
|
+
}
|
|
46
|
+
lane(index) {
|
|
47
|
+
return this.columns[index] ?? 0;
|
|
48
|
+
}
|
|
49
|
+
length() {
|
|
50
|
+
return this.extent;
|
|
51
|
+
}
|
|
52
|
+
from(offset) {
|
|
53
|
+
return this.search(this.reach, offset);
|
|
54
|
+
}
|
|
55
|
+
to(offset) {
|
|
56
|
+
return this.search(this.starts, offset - 1e-6);
|
|
57
|
+
}
|
|
58
|
+
search(sorted, offset) {
|
|
59
|
+
let low = 0;
|
|
60
|
+
let high = sorted.length;
|
|
61
|
+
while (low < high) {
|
|
62
|
+
const mid = (low + high) >> 1;
|
|
63
|
+
if ((sorted[mid] ?? 0) <= offset) {
|
|
64
|
+
low = mid + 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
high = mid;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return low;
|
|
71
|
+
}
|
|
72
|
+
step(key, at, count, page) {
|
|
73
|
+
const lane = this.lane(at);
|
|
74
|
+
const along = (way, from = at) => {
|
|
75
|
+
for (let other = from + way; other >= 0 && other < count; other += way) {
|
|
76
|
+
if (this.lane(other) === lane) {
|
|
77
|
+
return other;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
};
|
|
82
|
+
if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
83
|
+
return along(key === 'ArrowDown' ? 1 : -1);
|
|
84
|
+
}
|
|
85
|
+
if (key === 'PageDown' || key === 'PageUp') {
|
|
86
|
+
const way = key === 'PageDown' ? 1 : -1;
|
|
87
|
+
const goal = this.start(at) + way * Math.max(1, page);
|
|
88
|
+
let found = at;
|
|
89
|
+
for (let next = along(way); next !== undefined; next = along(way, next)) {
|
|
90
|
+
found = next;
|
|
91
|
+
if (way > 0 ? this.start(next) + this.size(next) > goal : this.start(next) <= goal) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return found === at ? undefined : found;
|
|
96
|
+
}
|
|
97
|
+
if (key !== 'next' && key !== 'previous') {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const beside = lane + (key === 'next' ? 1 : -1);
|
|
101
|
+
if (beside < 0 || beside >= Math.max(1, Math.trunc(this.lanes) || 1)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const top = this.start(at);
|
|
105
|
+
const bottom = top + this.size(at);
|
|
106
|
+
let best;
|
|
107
|
+
let most = 0;
|
|
108
|
+
let above;
|
|
109
|
+
for (let other = Math.max(0, this.from(top) - this.lanes); other < count; other++) {
|
|
110
|
+
const start = this.start(other);
|
|
111
|
+
if (start >= bottom) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (this.lane(other) !== beside) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const overlap = Math.min(bottom, start + this.size(other)) - Math.max(top, start);
|
|
118
|
+
if (overlap > most) {
|
|
119
|
+
best = other;
|
|
120
|
+
most = overlap;
|
|
121
|
+
}
|
|
122
|
+
above = other;
|
|
123
|
+
}
|
|
124
|
+
if (best !== undefined || above !== undefined) {
|
|
125
|
+
return best ?? above;
|
|
126
|
+
}
|
|
127
|
+
for (let other = at - 1; other >= 0; other--) {
|
|
128
|
+
if (this.lane(other) === beside) {
|
|
129
|
+
return other;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
size(index) {
|
|
135
|
+
return this.sizes[index] ?? 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -278,9 +278,19 @@ export declare class HelixElement extends LitElement {
|
|
|
278
278
|
* where you do.
|
|
279
279
|
*/
|
|
280
280
|
protected relayNativeEvent(event: Event, init?: EventInit): void;
|
|
281
|
+
/**
|
|
282
|
+
* The rules from `mark-rules.ts` for the marks this element writes in its
|
|
283
|
+
* own shadow root: `data-scroller`, `data-virtual`, `data-spoken` and
|
|
284
|
+
* `data-mirror`.
|
|
285
|
+
*
|
|
286
|
+
* A component that writes a mark names its rule here, and a subclass
|
|
287
|
+
* inherits the list. One that writes none takes only the three rules every
|
|
288
|
+
* shadow root gets, which is what keeps the other four out of a Button.
|
|
289
|
+
*/
|
|
290
|
+
protected static markRules: readonly string[];
|
|
281
291
|
/**
|
|
282
292
|
* Prepends the shadow-root reset to every subclass's styles, so no component
|
|
283
|
-
* has to remember to include it.
|
|
293
|
+
* has to remember to include it, with the rules for the marks it writes.
|
|
284
294
|
*/
|
|
285
295
|
protected static finalizeStyles(styles?: CSSResultOrNative | CSSResultOrNative[]): CSSResultOrNative[];
|
|
286
296
|
/**
|
|
@@ -5,12 +5,8 @@ import { noteVersion, reportUsage } from '../utils/report-usage/report-usage.js'
|
|
|
5
5
|
import { applyReactProperties, bindReactHandler, reactPropsOf } from './react-bridge.js';
|
|
6
6
|
const BOX_SIZING = '*,::before,::after{box-sizing:border-box}';
|
|
7
7
|
const HIDDEN = ":host([hidden]:not([hidden='until-found'])){display:none}";
|
|
8
|
-
const SCROLLER = '[data-scroller]{overflow:auto}';
|
|
9
|
-
const VIRTUAL = '[data-virtual]{overflow-anchor:none}';
|
|
10
|
-
const SPOKEN = '[data-spoken]{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}';
|
|
11
|
-
const MIRROR = ':host(:dir(rtl)) [data-mirror]{scale:-1 1}';
|
|
12
8
|
const SETTLED = ':host(:not(:state(settled))),:host(:not(:state(settled))) *{transition:none!important}';
|
|
13
|
-
const BASE = unsafeCSS(BOX_SIZING + HIDDEN +
|
|
9
|
+
const BASE = unsafeCSS(BOX_SIZING + HIDDEN + SETTLED);
|
|
14
10
|
const LOOK_EVERY = 16;
|
|
15
11
|
const LOOK_FOR = 96;
|
|
16
12
|
export class HelixElement extends LitElement {
|
|
@@ -94,8 +90,13 @@ export class HelixElement extends LitElement {
|
|
|
94
90
|
...init,
|
|
95
91
|
}));
|
|
96
92
|
}
|
|
93
|
+
static { this.markRules = []; }
|
|
97
94
|
static finalizeStyles(styles) {
|
|
98
|
-
|
|
95
|
+
const marks = this.markRules;
|
|
96
|
+
return [
|
|
97
|
+
marks.length === 0 ? BASE : unsafeCSS(BOX_SIZING + HIDDEN + marks.join('') + SETTLED),
|
|
98
|
+
...LitElement.finalizeStyles(styles),
|
|
99
|
+
];
|
|
99
100
|
}
|
|
100
101
|
performUpdate() {
|
|
101
102
|
if (this.hasUpdated || !measuring()) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes an element a scroll container, where a component says it is one.
|
|
3
|
+
*
|
|
4
|
+
* **A box that scrolls is the headless tier's, and the headless tier had no way to say
|
|
5
|
+
* so.** `overflow` is what creates a scrollport: without it `scrollTop` is
|
|
6
|
+
* permanently zero and every scroll API on the element is inert. So a component
|
|
7
|
+
* that reads `scrollTop` and cannot declare `overflow` is one whose behavior
|
|
8
|
+
* lives in somebody else's stylesheet.
|
|
9
|
+
*
|
|
10
|
+
* Measured before deciding: `helix-headless-scroll-area`, the component whose
|
|
11
|
+
* entire purpose is scrolling, computes `overflow-y: visible` on its viewport
|
|
12
|
+
* with `scrollHeight` equal to `clientHeight`. Standing alone it is a div, and
|
|
13
|
+
* the virtual list is worse because it *acts* on the reading.
|
|
14
|
+
*
|
|
15
|
+
* **It goes in `core` rather than in a stylesheet**, with `BOX_SIZING`,
|
|
16
|
+
* `HIDDEN` and `SETTLED`, which are the token-free structural rules `core`
|
|
17
|
+
* ships into every shadow root. "The headless tier has no CSS" has never meant
|
|
18
|
+
* `core` has none; it means `helix/headless` must not depend on
|
|
19
|
+
* `@fusion.dev/helix-styles`, and a rule with no token in it does not.
|
|
20
|
+
*
|
|
21
|
+
* **And a rule rather than an inline style, which was the first idea and is
|
|
22
|
+
* wrong.** An inline `style` on a rendered part outranks every author rule
|
|
23
|
+
* short of `!important`, so a consumer wanting `overflow: hidden` on that part
|
|
24
|
+
* would have to fight it and would need `!important` of their own. A shadow rule
|
|
25
|
+
* loses to a document rule, so `::part(viewport) { overflow: … }` from a
|
|
26
|
+
* consumer simply wins, which is the right way round.
|
|
27
|
+
*
|
|
28
|
+
* Both axes, and nothing else. Which *kind* of scrolling, how much room,
|
|
29
|
+
* `overscroll-behavior`, `scroll-snap`, a gutter, a scrollbar treatment: those
|
|
30
|
+
* are decisions and stay in `helix/styled`. This is only the mechanism, and the
|
|
31
|
+
* styled sheet is prepended-after so it overrides at equal specificity anyway.
|
|
32
|
+
*
|
|
33
|
+
* **A component marks a box whose scroll state it reads, and no other**, which
|
|
34
|
+
* is narrower than "every box that ends up scrolling". `Megamenu`'s panel
|
|
35
|
+
* scrolls, and only because the styled tier gives it a `max-block-size`: that is
|
|
36
|
+
* a decision, the `overflow` that serves it belongs with it, and the headless
|
|
37
|
+
* tier never asks the panel where it is scrolled to. Marking it anyway moved a
|
|
38
|
+
* heading four pixels, because `overflow` establishes a block formatting context
|
|
39
|
+
* and margins that had been collapsing stopped. A mechanism with a layout
|
|
40
|
+
* consequence is one to apply exactly where it is needed.
|
|
41
|
+
*
|
|
42
|
+
* `ScrollArea`, `VirtualList`, `Carousel`, `ObjectInspector`, `Chat` and a
|
|
43
|
+
* `windowed` `DataTable` mark theirs, each reading `scrollTop` or `scrollLeft`
|
|
44
|
+
* and acting on the answer. The last two took their `overflow` from their
|
|
45
|
+
* styled sheet until they were marked, so the window each drew was inert on a
|
|
46
|
+
* page that loads only `helix/headless`; marking them moved nothing on the
|
|
47
|
+
* styled tier, where both boxes already scrolled.
|
|
48
|
+
*/
|
|
49
|
+
export declare const SCROLLER_RULE = "[data-scroller]{overflow:auto}";
|
|
50
|
+
/**
|
|
51
|
+
* A box whose rows are drawn and taken away as it scrolls, which
|
|
52
|
+
* `VirtualizeController` marks `data-virtual` once it watches it.
|
|
53
|
+
*
|
|
54
|
+
* **The browser's scroll anchoring fights the window.** Chromium keeps the
|
|
55
|
+
* content a reader was looking at in place when what is above it changes, and
|
|
56
|
+
* a virtual window changes exactly that on every move: the spacer above grows
|
|
57
|
+
* and the rows are swapped. Measured on a 500 row list scrolled to 330, the
|
|
58
|
+
* offset went to 138, then 522, then back to 330 before it settled, and the
|
|
59
|
+
* list reported four ranges for one scroll. The window already keeps the rows
|
|
60
|
+
* where they belong, so anchoring has nothing left to do here but move them.
|
|
61
|
+
*/
|
|
62
|
+
export declare const VIRTUAL_RULE = "[data-virtual]{overflow-anchor:none}";
|
|
63
|
+
/**
|
|
64
|
+
* Words a screen reader reads where the eye reads something else, which a
|
|
65
|
+
* component marks `data-spoken`: `⇧⌘P` in Kbd is read as `Shift Command P`.
|
|
66
|
+
*
|
|
67
|
+
* The drawn text is `aria-hidden` and the words sit beside it, in no box the
|
|
68
|
+
* eye sees. The rule takes the words out of the layout and paints none of
|
|
69
|
+
* them, and names no token, so it belongs with the mechanisms here. Without
|
|
70
|
+
* it a page that loads only `helix/headless` showed both.
|
|
71
|
+
*/
|
|
72
|
+
export declare const SPOKEN_RULE = "[data-spoken]{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";
|
|
73
|
+
/**
|
|
74
|
+
* Turns a drawing that runs along the line of text, or a glyph that points
|
|
75
|
+
* along it, round with it on a right-to-left page.
|
|
76
|
+
*
|
|
77
|
+
* **A chart computes its geometry from the left, because an SVG's coordinates
|
|
78
|
+
* do.** `x` grows rightward whatever `direction` says, while the words around a
|
|
79
|
+
* drawing are HTML written in logical properties and turn on their own. Before
|
|
80
|
+
* this, a bar chart on an Arabic page wrote Q4 to Q1 from the left under
|
|
81
|
+
* columns still drawn Q1 to Q4, so each name stood under its mirror's column.
|
|
82
|
+
*
|
|
83
|
+
* **A rule rather than a second set of coordinates, and three things decide
|
|
84
|
+
* it.** A server renders the same markup whichever way the page reads, so the
|
|
85
|
+
* drawing it sends is right before a script runs. Everything drawn on a mark
|
|
86
|
+
* turns with it: a column rounded at its far end, a gradient toward its base,
|
|
87
|
+
* the hole a ring terminus cuts, each of which arithmetic would have to mirror
|
|
88
|
+
* again in every stylesheet that draws one. And it follows `dir` when it
|
|
89
|
+
* changes, which a render would only do on its next update.
|
|
90
|
+
*
|
|
91
|
+
* **About the vertical axis, never the horizontal.** Up is more in every
|
|
92
|
+
* script, so a value keeps its height and only the run along the line turns.
|
|
93
|
+
* The numbers are text beside the drawing rather than inside it, so they read
|
|
94
|
+
* the right way round.
|
|
95
|
+
*
|
|
96
|
+
* **Keyed on `:dir()`, which is what every sheet here mirrors a caret with.**
|
|
97
|
+
* The pointer arithmetic that has to agree with it asks the computed
|
|
98
|
+
* `direction`, the question the table and the splitter already ask, and the two
|
|
99
|
+
* part only where a sheet sets `direction` without `dir`.
|
|
100
|
+
*
|
|
101
|
+
* A component marks the drawing with `data-mirror`, and a drawing with no line
|
|
102
|
+
* of text to follow is left unmarked: a whole ring's slices run clockwise from
|
|
103
|
+
* twelve, which is a clock's convention and not a script's. `scale` rather than
|
|
104
|
+
* `transform`, so a consumer's `transform` on the same part composes with it,
|
|
105
|
+
* and a consumer's `scale` there wins, a shadow rule losing to a document one.
|
|
106
|
+
*
|
|
107
|
+
* **A glyph that points along the line is the same case, marked the same
|
|
108
|
+
* way**: a caret meaning next or previous, the one between two crumbs, a shut
|
|
109
|
+
* disclosure. The component drawing it knows what it means, so the headless
|
|
110
|
+
* tier points it the right way with no stylesheet at all. Before the mark each
|
|
111
|
+
* styled sheet turned its own and a headless page had to know to, which the
|
|
112
|
+
* carousel's and the pagination's did not. A glyph a *sheet* stands on its side
|
|
113
|
+
* is that sheet's to turn: a tree draws its chevron pointing down, and a mark
|
|
114
|
+
* would come out right under a `transform: rotate()`, which applies before
|
|
115
|
+
* `scale`, and do nothing under the `rotate` property, which applies after it.
|
|
116
|
+
* A glyph for playing media is left unmarked too, a tape running one way
|
|
117
|
+
* whatever the script, and so is a picture of a key.
|
|
118
|
+
*/
|
|
119
|
+
export declare const MIRROR_RULE = ":host(:dir(rtl)) [data-mirror]{scale:-1 1}";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const SCROLLER_RULE = '[data-scroller]{overflow:auto}';
|
|
2
|
+
export const VIRTUAL_RULE = '[data-virtual]{overflow-anchor:none}';
|
|
3
|
+
export const SPOKEN_RULE = '[data-spoken]{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}';
|
|
4
|
+
export const MIRROR_RULE = ':host(:dir(rtl)) [data-mirror]{scale:-1 1}';
|