@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.
Files changed (40) hide show
  1. package/custom-elements.json +1677 -349
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +4 -0
  3. package/dist/controllers/virtualize-controller/heights.d.ts +7 -0
  4. package/dist/controllers/virtualize-controller/heights.js +7 -0
  5. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +24 -0
  6. package/dist/controllers/virtualize-controller/virtualize-controller.js +40 -20
  7. package/dist/controllers/virtualize-controller/waterfall.d.ts +66 -0
  8. package/dist/controllers/virtualize-controller/waterfall.js +137 -0
  9. package/dist/helix-element/helix-element.d.ts +11 -1
  10. package/dist/helix-element/helix-element.js +7 -6
  11. package/dist/helix-element/mark-rules.d.ts +119 -0
  12. package/dist/helix-element/mark-rules.js +4 -0
  13. package/dist/helix-form-element/helix-form-element-base.d.ts +264 -0
  14. package/dist/helix-form-element/helix-form-element-base.js +173 -0
  15. package/dist/helix-form-element/helix-form-element.d.ts +8 -226
  16. package/dist/helix-form-element/helix-form-element.js +2 -157
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +3 -0
  19. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +1 -6
  20. package/dist/mixins/mixin-chart-status/mixin-chart-status.js +1 -6
  21. package/dist/mixins/mixin-collection/mixin-collection.js +1 -6
  22. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +1 -6
  23. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +1 -6
  24. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +1 -6
  25. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +1 -6
  26. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +1 -6
  27. package/dist/mixins/mixin-field/mixin-field.js +1 -6
  28. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +1 -6
  29. package/dist/mixins/mixin-picker-feedback/mixin-picker-feedback.js +1 -6
  30. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +1 -6
  31. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +1 -6
  32. package/dist/mixins/mixin-selectable-item/mixin-selectable-item.js +1 -6
  33. package/dist/mixins/mixin-tokens/mixin-tokens.js +1 -6
  34. package/dist/mixins/mixin-viewport/mixin-viewport.js +1 -6
  35. package/dist/utils/audit-name/audit-name.d.ts +3 -3
  36. package/dist/utils/audit-name/audit-name.js +7 -2
  37. package/dist/utils/implicit-submit/implicit-submit.js +4 -4
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. 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
- let next = this.measuring()
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 = this.measuring()
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 && (this.uneven || (this.options.measured?.() ?? true));
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 first = Math.max(0, this.heights.indexAt(from, total) - overscan);
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.heights.indexAt(top, total);
247
- while (last < total && this.heights.before(last, total) < bottom) {
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 + overscan);
267
+ last = Math.min(total, last + more);
251
268
  return {
252
- after: Math.max(0, this.heights.total(total) - this.heights.before(last, total)),
253
- before: this.heights.before(first, total) + this.drift,
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.heights.indexAt(top, total);
288
+ const first = this.indexAt(top, total);
272
289
  let last = first;
273
- while (last < total && this.heights.before(last, total) < bottom) {
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 length = this.measuring() ? this.heights.total(total) : total * this.line();
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.heights.indexAt(top, total);
336
- const was = this.heights.before(anchor, total);
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.heights.before(anchor, total) - was;
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.heights.before(by, count);
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.heights.before(index, total) + this.drift
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 + SCROLLER + VIRTUAL + SPOKEN + MIRROR + SETTLED);
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
- return [BASE, ...LitElement.finalizeStyles(styles)];
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}';