@fusion.dev/helix-core 0.0.0-dev
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/LICENSE +21 -0
- package/custom-elements.json +8005 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
- package/dist/controllers/command-controller/command-controller.d.ts +83 -0
- package/dist/controllers/command-controller/command-controller.js +93 -0
- package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
- package/dist/controllers/copy-controller/copy-controller.js +128 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
- package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
- package/dist/controllers/floating-controller/floating-controller.js +227 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
- package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
- package/dist/controllers/popover-controller/popover-controller.js +150 -0
- package/dist/controllers/source-controller/source-controller.d.ts +55 -0
- package/dist/controllers/source-controller/source-controller.js +94 -0
- package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
- package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
- package/dist/events/element-events.d.ts +4 -0
- package/dist/events/element-events.js +34 -0
- package/dist/events/events.d.ts +743 -0
- package/dist/events/events.js +799 -0
- package/dist/helix-element/helix-element.d.ts +201 -0
- package/dist/helix-element/helix-element.js +695 -0
- package/dist/helix-form-element/helix-form-element.d.ts +152 -0
- package/dist/helix-form-element/helix-form-element.js +194 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +54 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
- package/dist/mixins/mixin-collection/index.d.ts +1 -0
- package/dist/mixins/mixin-collection/index.js +1 -0
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
- package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
- package/dist/mixins/mixin-field/mixin-field.js +423 -0
- package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
- package/dist/utils/anchor-element/anchor-element.js +39 -0
- package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
- package/dist/utils/capture-pointer/capture-pointer.js +30 -0
- package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
- package/dist/utils/closest-instance/closest-instance.js +34 -0
- package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
- package/dist/utils/create-sheet/create-sheet.js +16 -0
- package/dist/utils/custom-element/custom-element.d.ts +26 -0
- package/dist/utils/custom-element/custom-element.js +114 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
- package/dist/utils/draw-icon/draw-icon.js +39 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
- package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
- package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
- package/dist/utils/elapsed-time/elapsed-time.js +117 -0
- package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
- package/dist/utils/focusable-elements/focusable-elements.js +88 -0
- package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
- package/dist/utils/format-numbers/format-numbers.js +29 -0
- package/dist/utils/host-aria/host-aria.d.ts +38 -0
- package/dist/utils/host-aria/host-aria.js +41 -0
- package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
- package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
- package/dist/utils/line-path/line-path.d.ts +23 -0
- package/dist/utils/line-path/line-path.js +107 -0
- package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
- package/dist/utils/on-unless-false/on-unless-false.js +32 -0
- package/dist/utils/read-series/read-series.d.ts +78 -0
- package/dist/utils/read-series/read-series.js +187 -0
- package/dist/utils/relative-time/relative-time.d.ts +24 -0
- package/dist/utils/relative-time/relative-time.js +84 -0
- package/dist/utils/request-change/request-change.d.ts +48 -0
- package/dist/utils/request-change/request-change.js +7 -0
- package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
- package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
- package/dist/utils/restore-value/restore-value.d.ts +32 -0
- package/dist/utils/restore-value/restore-value.js +84 -0
- package/dist/utils/scale/scale.d.ts +23 -0
- package/dist/utils/scale/scale.js +32 -0
- package/dist/utils/scale/ticks.d.ts +27 -0
- package/dist/utils/scale/ticks.js +52 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
- package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
- package/dist/utils/slot-filled/slot-filled.js +26 -0
- package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
- package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
- package/dist/utils/ticker/ticker.d.ts +8 -0
- package/dist/utils/ticker/ticker.js +55 -0
- package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
- package/dist/utils/tone-marks/tone-marks.js +36 -0
- package/dist/utils/value-as/value-as.d.ts +64 -0
- package/dist/utils/value-as/value-as.js +105 -0
- package/package.json +58 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DismissController } from '../dismiss-controller/dismiss-controller.js';
|
|
2
|
+
import { FloatingController } from '../floating-controller/floating-controller.js';
|
|
3
|
+
/**
|
|
4
|
+
* A surface that floats against something, in the top layer, until dismissed.
|
|
5
|
+
*
|
|
6
|
+
* Three things always travel together and were written out longhand three
|
|
7
|
+
* times: the popover API, a `FloatingController` and a `DismissController`.
|
|
8
|
+
* Individually each is small, which is exactly why the copies drifted rather
|
|
9
|
+
* than being noticed. **`Select` never called `showPopover` at all**, so its
|
|
10
|
+
* list was an ordinary positioned panel and any ancestor with `overflow:
|
|
11
|
+
* hidden` clipped it, while a menu two folders away in the same position did
|
|
12
|
+
* not. That is not a difference anyone chose.
|
|
13
|
+
*
|
|
14
|
+
* **The top layer is the whole point.** A floating panel has to escape every
|
|
15
|
+
* clipping ancestor between it and the page, and `position: fixed` does not do
|
|
16
|
+
* that: it is clipped by `overflow` on any ancestor and re-anchored entirely by
|
|
17
|
+
* an ancestor carrying `transform`, `filter` or `contain`. The popover API
|
|
18
|
+
* lifts the element out of the tree's paint order for real, and it is the only
|
|
19
|
+
* thing that does.
|
|
20
|
+
*
|
|
21
|
+
* `manual` rather than `auto`, because the host owns dismissal. `auto`
|
|
22
|
+
* popovers light-dismiss themselves and close each other, which is close to
|
|
23
|
+
* what is wanted and not the same: a select that closed itself before its own
|
|
24
|
+
* change event landed, or a submenu that closed its parent, are both things the
|
|
25
|
+
* platform would do here and neither is right.
|
|
26
|
+
*
|
|
27
|
+
* **The order matters on the way up.** `showPopover` first, then position:
|
|
28
|
+
* measuring an element that is still `display: none` gives zeroes, so a surface
|
|
29
|
+
* positioned before it is shown lands at the origin and slides into place on
|
|
30
|
+
* the next frame. On the way down it is the reverse, and stopping the observer
|
|
31
|
+
* before hiding avoids a final measurement of an element on its way out.
|
|
32
|
+
*
|
|
33
|
+
* What this does not own is *when*. Opening is the host's, because a menu opens
|
|
34
|
+
* on click, a tooltip on hover and a select on either, and none of that is
|
|
35
|
+
* about surfaces.
|
|
36
|
+
*/
|
|
37
|
+
export class PopoverController {
|
|
38
|
+
constructor(host, options) {
|
|
39
|
+
/** Whether the surface is currently shown, so the calls can be idempotent. */
|
|
40
|
+
this.shown = false;
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.floating = new FloatingController(host, {
|
|
43
|
+
anchor: options.anchor,
|
|
44
|
+
...(options.arrow === undefined ? {} : { arrow: options.arrow }),
|
|
45
|
+
floating: options.surface,
|
|
46
|
+
...(options.offset === undefined ? {} : { offset: options.offset }),
|
|
47
|
+
...(options.placement === undefined ? {} : { placement: options.placement }),
|
|
48
|
+
...(options.within === undefined ? {} : { within: options.within }),
|
|
49
|
+
});
|
|
50
|
+
this.dismisser = new DismissController(host, {
|
|
51
|
+
boundary: options.boundary ?? options.surface,
|
|
52
|
+
onDismiss: options.onDismiss,
|
|
53
|
+
});
|
|
54
|
+
host.addController(this);
|
|
55
|
+
}
|
|
56
|
+
/** Whether the surface is showing. */
|
|
57
|
+
get open() {
|
|
58
|
+
return this.shown;
|
|
59
|
+
}
|
|
60
|
+
/** Whether this one is placed by measurement, read fresh so it may change. */
|
|
61
|
+
get places() {
|
|
62
|
+
const asked = this.options.place;
|
|
63
|
+
if (asked === undefined) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return typeof asked === 'function' ? asked() : asked;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Shows the surface, positions it, and starts listening for a way out.
|
|
70
|
+
*
|
|
71
|
+
* Safe to call when already open, so a host may call it from whatever
|
|
72
|
+
* combination of a property, an event and a command it likes without keeping
|
|
73
|
+
* track of which one got there first.
|
|
74
|
+
*/
|
|
75
|
+
show() {
|
|
76
|
+
const surface = this.options.surface();
|
|
77
|
+
if (this.shown || !surface) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.shown = true;
|
|
81
|
+
/* Feature-detected rather than assumed. The popover API is recent enough
|
|
82
|
+
that a browser without it should still get a positioned panel, which is
|
|
83
|
+
what every one of these was before the API existed: worse, because it can
|
|
84
|
+
be clipped, and not broken. */
|
|
85
|
+
if (typeof surface.showPopover === 'function') {
|
|
86
|
+
/* And said plainly where the surface is not one. `showPopover` on an
|
|
87
|
+
element with no `popover` attribute throws `NotSupportedError`, from
|
|
88
|
+
inside the promise the host called this in, so it surfaces as an
|
|
89
|
+
unhandled rejection naming a method rather than as the thing that is
|
|
90
|
+
actually wrong. Which is how `Select` looked for as long as it was
|
|
91
|
+
broken: no error at all, just a panel that could be clipped. */
|
|
92
|
+
if (!surface.hasAttribute('popover')) {
|
|
93
|
+
throw new Error('PopoverController: the surface needs a `popover` attribute to reach the top layer');
|
|
94
|
+
}
|
|
95
|
+
surface.showPopover();
|
|
96
|
+
}
|
|
97
|
+
if (this.places) {
|
|
98
|
+
this.floating.start();
|
|
99
|
+
}
|
|
100
|
+
this.dismisser.activate();
|
|
101
|
+
}
|
|
102
|
+
/** Hides the surface and stops everything watching it. */
|
|
103
|
+
hide() {
|
|
104
|
+
const surface = this.options.surface();
|
|
105
|
+
if (!this.shown) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.shown = false;
|
|
109
|
+
this.dismisser.deactivate();
|
|
110
|
+
this.floating.stop();
|
|
111
|
+
surface?.hidePopover?.();
|
|
112
|
+
}
|
|
113
|
+
/** Whichever of the two the surface is not already doing. */
|
|
114
|
+
toggle() {
|
|
115
|
+
if (this.shown) {
|
|
116
|
+
this.hide();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.show();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Places the surface again, for an anchor that moved without the page moving.
|
|
124
|
+
*
|
|
125
|
+
* `autoUpdate` watches scrolling, resizing and layout shift, which covers
|
|
126
|
+
* everything that moves an anchor *with* its surroundings. It does not cover
|
|
127
|
+
* an absolutely positioned anchor being moved by an inline style, and that is
|
|
128
|
+
* exactly what a chart does: the mark walks from reading to reading while the
|
|
129
|
+
* page holds still, so the bubble was placed once and stayed there.
|
|
130
|
+
*/
|
|
131
|
+
reposition() {
|
|
132
|
+
this.floating.update();
|
|
133
|
+
}
|
|
134
|
+
/*
|
|
135
|
+
* Forgets that it was showing, and nothing more, which is the whole job.
|
|
136
|
+
*
|
|
137
|
+
* An element removed while its popover is showing is taken out of the top
|
|
138
|
+
* layer by the platform. The two controllers watching it stop themselves:
|
|
139
|
+
* both were constructed against this same host and registered with it, so Lit
|
|
140
|
+
* calls each one's own `hostDisconnected`, and `autoUpdate` in particular
|
|
141
|
+
* releases the listeners it holds on every scrolling ancestor there.
|
|
142
|
+
*
|
|
143
|
+
* Stopping them from here as well would be the same call twice. What is left
|
|
144
|
+
* is the flag, which nothing else owns, and which has to be cleared or a host
|
|
145
|
+
* reconnected to the page believes it is still open and refuses to show.
|
|
146
|
+
*/
|
|
147
|
+
hostDisconnected() {
|
|
148
|
+
this.shown = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type Series } from '../../utils/read-series/read-series.js';
|
|
3
|
+
/** A host that names the element it reads from. */
|
|
4
|
+
export interface SourceHost extends ReactiveControllerHost, HTMLElement {
|
|
5
|
+
/**
|
|
6
|
+
* The id of the table or grid to read.
|
|
7
|
+
*/
|
|
8
|
+
for: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the element a chart reads from, and follows it.
|
|
12
|
+
*
|
|
13
|
+
* **Three charts had written this out separately before it was one thing**, and
|
|
14
|
+
* the drift had already started: two of them called the lookup `#table` and one
|
|
15
|
+
* called it `#source`, for the same method with the same body. Every chart that
|
|
16
|
+
* takes a `for` wants exactly this, and wants it to behave identically, so it is
|
|
17
|
+
* a controller rather than a paragraph in each of them.
|
|
18
|
+
*
|
|
19
|
+
* A controller and not a pure function, by the test the rules set: it reacts to
|
|
20
|
+
* the host's lifecycle. It has to start watching when the host connects, stop
|
|
21
|
+
* when it leaves, and re-resolve when `for` changes.
|
|
22
|
+
*
|
|
23
|
+
* **What it refuses is as much the point as what it accepts.** A `<table>` or
|
|
24
|
+
* anything built from grid roles, including this library's own `helix-table`,
|
|
25
|
+
* whose roles live in `ElementInternals` and therefore write no attribute at
|
|
26
|
+
* all. Anything else resolves to null, so a `for` pointed at a paragraph draws
|
|
27
|
+
* nothing rather than something arbitrary.
|
|
28
|
+
*
|
|
29
|
+
* The element is watched for `childList` and `characterData` alike, because
|
|
30
|
+
* renaming a series edits a text node in place rather than replacing a cell,
|
|
31
|
+
* and nothing in a page has reason to know a chart is reading it.
|
|
32
|
+
*/
|
|
33
|
+
export declare class SourceController implements ReactiveController {
|
|
34
|
+
#private;
|
|
35
|
+
constructor(host: SourceHost);
|
|
36
|
+
/**
|
|
37
|
+
* The element `for` names, or null where there is nothing to name it in.
|
|
38
|
+
*
|
|
39
|
+
* Null on the server as well, since resolving an id wants a document. Every
|
|
40
|
+
* chart inherits that: the table is served whole and the drawing arrives on
|
|
41
|
+
* hydration.
|
|
42
|
+
*/
|
|
43
|
+
get element(): Element | null;
|
|
44
|
+
/** The series it holds, or none. */
|
|
45
|
+
get series(): Series[];
|
|
46
|
+
hostConnected(): void;
|
|
47
|
+
hostDisconnected(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Points the watcher at whatever `for` names now.
|
|
50
|
+
*
|
|
51
|
+
* Called on connect and by the host when `for` changes. Safe to call when
|
|
52
|
+
* there is no `MutationObserver`, which is the server.
|
|
53
|
+
*/
|
|
54
|
+
watch(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readSeries } from '../../utils/read-series/read-series.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the element a chart reads from, and follows it.
|
|
4
|
+
*
|
|
5
|
+
* **Three charts had written this out separately before it was one thing**, and
|
|
6
|
+
* the drift had already started: two of them called the lookup `#table` and one
|
|
7
|
+
* called it `#source`, for the same method with the same body. Every chart that
|
|
8
|
+
* takes a `for` wants exactly this, and wants it to behave identically, so it is
|
|
9
|
+
* a controller rather than a paragraph in each of them.
|
|
10
|
+
*
|
|
11
|
+
* A controller and not a pure function, by the test the rules set: it reacts to
|
|
12
|
+
* the host's lifecycle. It has to start watching when the host connects, stop
|
|
13
|
+
* when it leaves, and re-resolve when `for` changes.
|
|
14
|
+
*
|
|
15
|
+
* **What it refuses is as much the point as what it accepts.** A `<table>` or
|
|
16
|
+
* anything built from grid roles, including this library's own `helix-table`,
|
|
17
|
+
* whose roles live in `ElementInternals` and therefore write no attribute at
|
|
18
|
+
* all. Anything else resolves to null, so a `for` pointed at a paragraph draws
|
|
19
|
+
* nothing rather than something arbitrary.
|
|
20
|
+
*
|
|
21
|
+
* The element is watched for `childList` and `characterData` alike, because
|
|
22
|
+
* renaming a series edits a text node in place rather than replacing a cell,
|
|
23
|
+
* and nothing in a page has reason to know a chart is reading it.
|
|
24
|
+
*/
|
|
25
|
+
export class SourceController {
|
|
26
|
+
#host;
|
|
27
|
+
#observer;
|
|
28
|
+
constructor(host) {
|
|
29
|
+
this.#host = host;
|
|
30
|
+
host.addController(this);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The element `for` names, or null where there is nothing to name it in.
|
|
34
|
+
*
|
|
35
|
+
* Null on the server as well, since resolving an id wants a document. Every
|
|
36
|
+
* chart inherits that: the table is served whole and the drawing arrives on
|
|
37
|
+
* hydration.
|
|
38
|
+
*/
|
|
39
|
+
get element() {
|
|
40
|
+
const host = this.#host;
|
|
41
|
+
if (host.for === '') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const root = host.getRootNode();
|
|
45
|
+
const found = root.getElementById?.(host.for);
|
|
46
|
+
if (!found) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/* `tagName` rather than `instanceof HTMLTableElement`, which is not a guard
|
|
50
|
+
on the server: the constructor is not defined there, so naming it throws
|
|
51
|
+
before the comparison can answer false. It is also the check that
|
|
52
|
+
survives an element from another document. */
|
|
53
|
+
const role = found.getAttribute('role');
|
|
54
|
+
return found.tagName === 'TABLE' ||
|
|
55
|
+
role === 'table' ||
|
|
56
|
+
role === 'grid' ||
|
|
57
|
+
/^helix-(headless-)?table$/i.test(found.tagName)
|
|
58
|
+
? found
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
/** The series it holds, or none. */
|
|
62
|
+
get series() {
|
|
63
|
+
const element = this.element;
|
|
64
|
+
return element ? readSeries(element) : [];
|
|
65
|
+
}
|
|
66
|
+
hostConnected() {
|
|
67
|
+
this.watch();
|
|
68
|
+
}
|
|
69
|
+
hostDisconnected() {
|
|
70
|
+
this.#observer?.disconnect();
|
|
71
|
+
this.#observer = undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Points the watcher at whatever `for` names now.
|
|
75
|
+
*
|
|
76
|
+
* Called on connect and by the host when `for` changes. Safe to call when
|
|
77
|
+
* there is no `MutationObserver`, which is the server.
|
|
78
|
+
*/
|
|
79
|
+
watch() {
|
|
80
|
+
this.#observer?.disconnect();
|
|
81
|
+
const element = this.element;
|
|
82
|
+
if (!element || typeof MutationObserver === 'undefined') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.#observer ??= new MutationObserver(() => {
|
|
86
|
+
this.#host.requestUpdate();
|
|
87
|
+
});
|
|
88
|
+
this.#observer.observe(element, {
|
|
89
|
+
characterData: true,
|
|
90
|
+
childList: true,
|
|
91
|
+
subtree: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
/** A color as the three channels in `0..1` that a shader or a canvas takes. */
|
|
3
|
+
export type TokenColor = [number, number, number];
|
|
4
|
+
/**
|
|
5
|
+
* Reads a design token as the sRGB triple a shader or a 2D canvas can take, and
|
|
6
|
+
* tells the host when the palette underneath it moves.
|
|
7
|
+
*
|
|
8
|
+
* **Two hops, and both are load-bearing.** A custom property's own value is the
|
|
9
|
+
* unevaluated function text, so `getComputedStyle` on a probe is the only way
|
|
10
|
+
* to see what it resolves to. And what it resolves to is not `rgb()`: the
|
|
11
|
+
* palette derives from an `oklch()` seed, and Chrome answers `lab(100 0 0)` for
|
|
12
|
+
* white. Parsing that with a regular expression for numbers reads the three
|
|
13
|
+
* channels as 100, 0 and 0, which divided by 255 is a dark red.
|
|
14
|
+
*
|
|
15
|
+
* That is not hypothetical. It shipped, and `--helix-color-foreground` arrived
|
|
16
|
+
* as a dark red that painted a warm streak down a shader for an afternoon. It
|
|
17
|
+
* survived review because a warm cast on a saturated gradient is also what a
|
|
18
|
+
* screenshot's chroma subsampling produces, so the picture agreed with the bug.
|
|
19
|
+
*
|
|
20
|
+
* The conversion is therefore the browser's own: fill a pixel and read it back.
|
|
21
|
+
* That holds for `lab()`, `oklch()`, `color-mix()` and whatever a token is
|
|
22
|
+
* written in next, which is the property worth having in a design system whose
|
|
23
|
+
* palette is derived rather than listed.
|
|
24
|
+
*
|
|
25
|
+
* **A controller and not a mixin**, by this repository's own line: it adds no
|
|
26
|
+
* property or method a consumer sets. It reacts to lifecycle, holding a probe
|
|
27
|
+
* that has to be created after the host connects and removed when it leaves,
|
|
28
|
+
* and it watches for the color mode changing so the host can repaint.
|
|
29
|
+
*
|
|
30
|
+
* **Client only, and that is the contract rather than a limitation.** There is
|
|
31
|
+
* no computed style on a server and no canvas to convert with, so `read`
|
|
32
|
+
* answers the fallback there. Every caller is drawing into a canvas, which does
|
|
33
|
+
* not exist on a server either.
|
|
34
|
+
*/
|
|
35
|
+
export declare class TokenColorController implements ReactiveController {
|
|
36
|
+
private readonly host;
|
|
37
|
+
private readonly onChange?;
|
|
38
|
+
/** Where a resolved color is converted, one pixel at a time. */
|
|
39
|
+
private context;
|
|
40
|
+
/** Watches the document for `data-color-mode`. */
|
|
41
|
+
private modes;
|
|
42
|
+
/** The hidden element a token is resolved against. */
|
|
43
|
+
private probe;
|
|
44
|
+
/** Watches the operating system's answer, where nothing overrides it. */
|
|
45
|
+
private scheme;
|
|
46
|
+
/**
|
|
47
|
+
* @param host The element to update when the palette changes.
|
|
48
|
+
* @param onChange Called when the mode moves, after the host has updated. A
|
|
49
|
+
* host that paints outside Lit's cycle needs to know, because a canvas
|
|
50
|
+
* holds its last frame and no re-render repaints it.
|
|
51
|
+
*/
|
|
52
|
+
constructor(host: ReactiveControllerHost, onChange?: (() => void) | undefined);
|
|
53
|
+
hostConnected(): void;
|
|
54
|
+
hostDisconnected(): void;
|
|
55
|
+
/**
|
|
56
|
+
* The token's current value, or the fallback where it cannot be resolved.
|
|
57
|
+
*
|
|
58
|
+
* @param token The custom property's name, including its leading dashes.
|
|
59
|
+
* @param fallback What to answer on a server, or where the token is unset.
|
|
60
|
+
*/
|
|
61
|
+
read(token: string, fallback: TokenColor): TokenColor;
|
|
62
|
+
private readonly announce;
|
|
63
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a design token as the sRGB triple a shader or a 2D canvas can take, and
|
|
3
|
+
* tells the host when the palette underneath it moves.
|
|
4
|
+
*
|
|
5
|
+
* **Two hops, and both are load-bearing.** A custom property's own value is the
|
|
6
|
+
* unevaluated function text, so `getComputedStyle` on a probe is the only way
|
|
7
|
+
* to see what it resolves to. And what it resolves to is not `rgb()`: the
|
|
8
|
+
* palette derives from an `oklch()` seed, and Chrome answers `lab(100 0 0)` for
|
|
9
|
+
* white. Parsing that with a regular expression for numbers reads the three
|
|
10
|
+
* channels as 100, 0 and 0, which divided by 255 is a dark red.
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical. It shipped, and `--helix-color-foreground` arrived
|
|
13
|
+
* as a dark red that painted a warm streak down a shader for an afternoon. It
|
|
14
|
+
* survived review because a warm cast on a saturated gradient is also what a
|
|
15
|
+
* screenshot's chroma subsampling produces, so the picture agreed with the bug.
|
|
16
|
+
*
|
|
17
|
+
* The conversion is therefore the browser's own: fill a pixel and read it back.
|
|
18
|
+
* That holds for `lab()`, `oklch()`, `color-mix()` and whatever a token is
|
|
19
|
+
* written in next, which is the property worth having in a design system whose
|
|
20
|
+
* palette is derived rather than listed.
|
|
21
|
+
*
|
|
22
|
+
* **A controller and not a mixin**, by this repository's own line: it adds no
|
|
23
|
+
* property or method a consumer sets. It reacts to lifecycle, holding a probe
|
|
24
|
+
* that has to be created after the host connects and removed when it leaves,
|
|
25
|
+
* and it watches for the color mode changing so the host can repaint.
|
|
26
|
+
*
|
|
27
|
+
* **Client only, and that is the contract rather than a limitation.** There is
|
|
28
|
+
* no computed style on a server and no canvas to convert with, so `read`
|
|
29
|
+
* answers the fallback there. Every caller is drawing into a canvas, which does
|
|
30
|
+
* not exist on a server either.
|
|
31
|
+
*/
|
|
32
|
+
export class TokenColorController {
|
|
33
|
+
/**
|
|
34
|
+
* @param host The element to update when the palette changes.
|
|
35
|
+
* @param onChange Called when the mode moves, after the host has updated. A
|
|
36
|
+
* host that paints outside Lit's cycle needs to know, because a canvas
|
|
37
|
+
* holds its last frame and no re-render repaints it.
|
|
38
|
+
*/
|
|
39
|
+
constructor(host, onChange) {
|
|
40
|
+
this.host = host;
|
|
41
|
+
this.onChange = onChange;
|
|
42
|
+
/** Where a resolved color is converted, one pixel at a time. */
|
|
43
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: read in three places below, and this rule's own automatic fix removed it, which failed `typecheck` immediately. The suppression has to sit under the doc comment rather than over it, being directional.
|
|
44
|
+
this.context = null;
|
|
45
|
+
this.announce = () => {
|
|
46
|
+
this.host.requestUpdate();
|
|
47
|
+
this.onChange?.();
|
|
48
|
+
};
|
|
49
|
+
host.addController(this);
|
|
50
|
+
}
|
|
51
|
+
hostConnected() {
|
|
52
|
+
/* Guarded on `document` rather than on `window`, which the server shim does
|
|
53
|
+
provide. There is nothing to append a probe to without one. */
|
|
54
|
+
if (typeof document === 'undefined') {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.probe = document.createElement('span');
|
|
58
|
+
this.probe.style.display = 'none';
|
|
59
|
+
document.body.appendChild(this.probe);
|
|
60
|
+
const swatch = document.createElement('canvas');
|
|
61
|
+
swatch.width = 1;
|
|
62
|
+
swatch.height = 1;
|
|
63
|
+
this.context = swatch.getContext('2d', { willReadFrequently: true });
|
|
64
|
+
/* The color mode is an attribute, and the system's answer is a media query.
|
|
65
|
+
Watching only the first leaves a reader on system light with the dark
|
|
66
|
+
palette, which is the half that is easy to miss because a developer
|
|
67
|
+
toggling the site's own control never sees it. */
|
|
68
|
+
this.modes = new MutationObserver(() => this.announce());
|
|
69
|
+
this.modes.observe(document.documentElement, {
|
|
70
|
+
attributeFilter: ['data-color-mode'],
|
|
71
|
+
attributes: true,
|
|
72
|
+
});
|
|
73
|
+
this.scheme = window.matchMedia('(prefers-color-scheme: light)');
|
|
74
|
+
this.scheme.addEventListener('change', this.announce);
|
|
75
|
+
}
|
|
76
|
+
hostDisconnected() {
|
|
77
|
+
this.modes?.disconnect();
|
|
78
|
+
this.modes = undefined;
|
|
79
|
+
this.scheme?.removeEventListener('change', this.announce);
|
|
80
|
+
this.scheme = undefined;
|
|
81
|
+
this.probe?.remove();
|
|
82
|
+
this.probe = undefined;
|
|
83
|
+
this.context = null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The token's current value, or the fallback where it cannot be resolved.
|
|
87
|
+
*
|
|
88
|
+
* @param token The custom property's name, including its leading dashes.
|
|
89
|
+
* @param fallback What to answer on a server, or where the token is unset.
|
|
90
|
+
*/
|
|
91
|
+
read(token, fallback) {
|
|
92
|
+
const { context, probe } = this;
|
|
93
|
+
if (!context || !probe) {
|
|
94
|
+
return [...fallback];
|
|
95
|
+
}
|
|
96
|
+
probe.style.color = `var(${token})`;
|
|
97
|
+
/* Reset first. An unparseable value leaves `fillStyle` where it was, which
|
|
98
|
+
would silently hand back the previous token's color. */
|
|
99
|
+
context.fillStyle = '#000000';
|
|
100
|
+
context.fillStyle = getComputedStyle(probe).color;
|
|
101
|
+
context.fillRect(0, 0, 1, 1);
|
|
102
|
+
const [r, g, b] = context.getImageData(0, 0, 1, 1).data;
|
|
103
|
+
return [(r ?? 0) / 255, (g ?? 0) / 255, (b ?? 0) / 255];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
/** What the controller needs of a set and the box it is shown in. */
|
|
3
|
+
export interface VirtualizeOptions {
|
|
4
|
+
/** The scrolling box, or null before it renders. */
|
|
5
|
+
container: () => HTMLElement | null | undefined;
|
|
6
|
+
/** How many items there are in total, shown or not. */
|
|
7
|
+
count: () => number;
|
|
8
|
+
/** How tall one item is along the scroll axis, in pixels. */
|
|
9
|
+
estimate: () => number;
|
|
10
|
+
/**
|
|
11
|
+
* Rows kept either side of what fits, so a scroll reveals something drawn
|
|
12
|
+
* rather than something blank. Four by default, which is roughly a frame of
|
|
13
|
+
* fast scrolling at a 32 pixel row.
|
|
14
|
+
*/
|
|
15
|
+
overscan?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Which slice is rendered, and how much empty space stands in for the rest. */
|
|
18
|
+
export interface Window {
|
|
19
|
+
/** How much room the items before `first` would have taken, in pixels. */
|
|
20
|
+
before: number;
|
|
21
|
+
/** How much room the items after `last` would take, in pixels. */
|
|
22
|
+
after: number;
|
|
23
|
+
/** Index of the first item rendered. */
|
|
24
|
+
first: number;
|
|
25
|
+
/** Index one past the last item rendered. */
|
|
26
|
+
last: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Works out which slice of a long set is worth rendering.
|
|
30
|
+
*
|
|
31
|
+
* **A controller and not an element, which is the whole reason it is here.**
|
|
32
|
+
* Table, Listbox, Combobox and Tree each need windowing *inside* themselves,
|
|
33
|
+
* and an element would make each of them nest one in its own shadow root: a
|
|
34
|
+
* listbox whose options are inside a `<helix-virtual-list>` is a listbox whose
|
|
35
|
+
* options are no longer its children, so its own keyboard, its own selection
|
|
36
|
+
* and its own ARIA all have a stranger in the middle of them.
|
|
37
|
+
*
|
|
38
|
+
* **A windowed set still reports its true length, and each row its true
|
|
39
|
+
* position.** This is the part virtualization usually gets wrong and the reason
|
|
40
|
+
* the arithmetic lives somewhere shared. A reader on row 4,000 of 10,000 must
|
|
41
|
+
* hear "4,000 of 10,000", so `aria-setsize` is the count and `aria-posinset` is
|
|
42
|
+
* the real index, never the position within the rendered slice. The window is a
|
|
43
|
+
* rendering decision and the set is a fact, and a component that lets the first
|
|
44
|
+
* overwrite the second has told a screen reader the list is twelve long.
|
|
45
|
+
*
|
|
46
|
+
* **The spacers are what make the scrollbar honest.** Rendering twelve rows in
|
|
47
|
+
* a box that should scroll for ten thousand gives a thumb the size of the
|
|
48
|
+
* track, so the room the absent rows would have taken is left as padding before
|
|
49
|
+
* and after. A reader dragging that thumb is dragging against the real length.
|
|
50
|
+
*
|
|
51
|
+
* **What it does not do, and the reason is narrower than it first looks.** True
|
|
52
|
+
* virtualization means never creating the rows that are not on screen, and that
|
|
53
|
+
* needs the set as *data*. Two channels are shut: a consumer's items arrive as
|
|
54
|
+
* light-DOM children, which exist by the time this could decide not to create
|
|
55
|
+
* them, and an `items` property cannot survive React hydration, which skips
|
|
56
|
+
* every property it would have to apply imperatively (see `bindReactHandler`,
|
|
57
|
+
* and the note in the rules that objects and functions have that bug and are
|
|
58
|
+
* not fixed).
|
|
59
|
+
*
|
|
60
|
+
* **A third is open, and this said otherwise until 21 Aug 2026.** Data as *text*
|
|
61
|
+
* in the markup, a `<script type="application/json">` child or an attribute, is
|
|
62
|
+
* neither a property nor a node per row: React's hydration bug cannot touch text
|
|
63
|
+
* it renders as a child, a server emits it, and the node count is one. The
|
|
64
|
+
* charts already read data out of the document this way, `for="<id>"` at a real
|
|
65
|
+
* `<table>`, so the pattern is in the library rather than being invented for
|
|
66
|
+
* this.
|
|
67
|
+
*
|
|
68
|
+
* So the thing actually blocked is the *row template*. Turning an item into
|
|
69
|
+
* markup wants either a render function, which is a function property and dies
|
|
70
|
+
* on the same bug, or a `<template>` with named holes, which survives because it
|
|
71
|
+
* is markup and which no component here asks anyone to write. That is a decision
|
|
72
|
+
* about what this library is willing to look like, not a platform limit, and it
|
|
73
|
+
* is open at #71.
|
|
74
|
+
*
|
|
75
|
+
* So what this buys is the expensive half rather than all of it: the rows
|
|
76
|
+
* outside the window are not laid out and not painted, which is where the time
|
|
77
|
+
* goes in a long list, and the node count stays. That is worth having and it is
|
|
78
|
+
* not worth calling something it is not. An element that windows children it
|
|
79
|
+
* was handed is not a virtual list, whatever it is named.
|
|
80
|
+
*
|
|
81
|
+
* **One estimate, not per-row measurement.** Measuring every row to place it is
|
|
82
|
+
* how a virtual list ends up reading the layout it is trying to avoid, and it
|
|
83
|
+
* is what makes them jump: a row taller than its estimate shifts everything
|
|
84
|
+
* after it at the moment it renders. A fixed estimate is a straight line
|
|
85
|
+
* between scroll offset and index, which never disagrees with itself. Rows of
|
|
86
|
+
* genuinely different heights are a different component and a harder one.
|
|
87
|
+
*/
|
|
88
|
+
export declare class VirtualizeController implements ReactiveController {
|
|
89
|
+
private readonly host;
|
|
90
|
+
private readonly options;
|
|
91
|
+
private observer;
|
|
92
|
+
private watching;
|
|
93
|
+
/** Consecutive measurements that moved the window with nothing outside asking. */
|
|
94
|
+
private settling;
|
|
95
|
+
/** Whether the consumer has been told their box cannot be measured. */
|
|
96
|
+
private warned;
|
|
97
|
+
/** The host's own inputs, so a change in them is not read as a runaway. */
|
|
98
|
+
private last;
|
|
99
|
+
/** The slice to render, recomputed on scroll and on resize. */
|
|
100
|
+
window: Window;
|
|
101
|
+
constructor(host: ReactiveControllerHost, options: VirtualizeOptions);
|
|
102
|
+
hostConnected(): void;
|
|
103
|
+
hostDisconnected(): void;
|
|
104
|
+
/** Attaches to the box the first time there is one to attach to. */
|
|
105
|
+
private watch;
|
|
106
|
+
private readonly handleScroll;
|
|
107
|
+
/**
|
|
108
|
+
* Reads the scroll offset and settles the window.
|
|
109
|
+
*
|
|
110
|
+
* Asks for an update only when the slice actually moved. A scroll fires many
|
|
111
|
+
* events per frame and most of them land inside the row already at the top,
|
|
112
|
+
* so requesting one per event would re-render the list for every pixel.
|
|
113
|
+
*/
|
|
114
|
+
measure(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Says once that the box cannot be measured, and why.
|
|
117
|
+
*
|
|
118
|
+
* A warning rather than a throw, because the list still works: it holds a
|
|
119
|
+
* window, it draws rows, and a reader can scroll. What it cannot do is choose
|
|
120
|
+
* the window well, and that is the consumer's stylesheet to fix, not
|
|
121
|
+
* something this can decide on their behalf.
|
|
122
|
+
*
|
|
123
|
+
* Once per controller, because a warning per frame is one people filter out
|
|
124
|
+
* and this one has to be believed the day it is right.
|
|
125
|
+
*/
|
|
126
|
+
private report;
|
|
127
|
+
}
|