@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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many times an update this asked for may change the answer again.
|
|
3
|
+
*
|
|
4
|
+
* **A measurement that its own render moves has no fixed point, and nothing
|
|
5
|
+
* here could previously stop looking for one.** `clientHeight` is meant to be a
|
|
6
|
+
* fact about the box, true of any real scroller, and false of a box with no
|
|
7
|
+
* height: there it is a function of the rows just drawn, so `measure` renders,
|
|
8
|
+
* the render changes the height, and the next measurement disagrees.
|
|
9
|
+
*
|
|
10
|
+
* It does not even diverge, which is what made it hard to see. Measured on the
|
|
11
|
+
* headless tier with 500 rows and no height on the viewport, it converged
|
|
12
|
+
* toward a non-integer and then oscillated between 378 and 379 rows forever,
|
|
13
|
+
* 2505 passes in eight seconds. The loop runs on microtasks, so timers never
|
|
14
|
+
* fire, and every deadline written to catch this, the harness's 2000ms race and
|
|
15
|
+
* the spec's own bounded `settle`, is a `setTimeout` that cannot run. The page
|
|
16
|
+
* simply stops.
|
|
17
|
+
*
|
|
18
|
+
* Ten, because a legitimate settling is two or three: nothing, then the real
|
|
19
|
+
* height, then agreement.
|
|
20
|
+
*/
|
|
21
|
+
const UNSTABLE = 10;
|
|
22
|
+
/**
|
|
23
|
+
* Works out which slice of a long set is worth rendering.
|
|
24
|
+
*
|
|
25
|
+
* **A controller and not an element, which is the whole reason it is here.**
|
|
26
|
+
* Table, Listbox, Combobox and Tree each need windowing *inside* themselves,
|
|
27
|
+
* and an element would make each of them nest one in its own shadow root: a
|
|
28
|
+
* listbox whose options are inside a `<helix-virtual-list>` is a listbox whose
|
|
29
|
+
* options are no longer its children, so its own keyboard, its own selection
|
|
30
|
+
* and its own ARIA all have a stranger in the middle of them.
|
|
31
|
+
*
|
|
32
|
+
* **A windowed set still reports its true length, and each row its true
|
|
33
|
+
* position.** This is the part virtualization usually gets wrong and the reason
|
|
34
|
+
* the arithmetic lives somewhere shared. A reader on row 4,000 of 10,000 must
|
|
35
|
+
* hear "4,000 of 10,000", so `aria-setsize` is the count and `aria-posinset` is
|
|
36
|
+
* the real index, never the position within the rendered slice. The window is a
|
|
37
|
+
* rendering decision and the set is a fact, and a component that lets the first
|
|
38
|
+
* overwrite the second has told a screen reader the list is twelve long.
|
|
39
|
+
*
|
|
40
|
+
* **The spacers are what make the scrollbar honest.** Rendering twelve rows in
|
|
41
|
+
* a box that should scroll for ten thousand gives a thumb the size of the
|
|
42
|
+
* track, so the room the absent rows would have taken is left as padding before
|
|
43
|
+
* and after. A reader dragging that thumb is dragging against the real length.
|
|
44
|
+
*
|
|
45
|
+
* **What it does not do, and the reason is narrower than it first looks.** True
|
|
46
|
+
* virtualization means never creating the rows that are not on screen, and that
|
|
47
|
+
* needs the set as *data*. Two channels are shut: a consumer's items arrive as
|
|
48
|
+
* light-DOM children, which exist by the time this could decide not to create
|
|
49
|
+
* them, and an `items` property cannot survive React hydration, which skips
|
|
50
|
+
* every property it would have to apply imperatively (see `bindReactHandler`,
|
|
51
|
+
* and the note in the rules that objects and functions have that bug and are
|
|
52
|
+
* not fixed).
|
|
53
|
+
*
|
|
54
|
+
* **A third is open, and this said otherwise until 21 Aug 2026.** Data as *text*
|
|
55
|
+
* in the markup, a `<script type="application/json">` child or an attribute, is
|
|
56
|
+
* neither a property nor a node per row: React's hydration bug cannot touch text
|
|
57
|
+
* it renders as a child, a server emits it, and the node count is one. The
|
|
58
|
+
* charts already read data out of the document this way, `for="<id>"` at a real
|
|
59
|
+
* `<table>`, so the pattern is in the library rather than being invented for
|
|
60
|
+
* this.
|
|
61
|
+
*
|
|
62
|
+
* So the thing actually blocked is the *row template*. Turning an item into
|
|
63
|
+
* markup wants either a render function, which is a function property and dies
|
|
64
|
+
* on the same bug, or a `<template>` with named holes, which survives because it
|
|
65
|
+
* is markup and which no component here asks anyone to write. That is a decision
|
|
66
|
+
* about what this library is willing to look like, not a platform limit, and it
|
|
67
|
+
* is open at #71.
|
|
68
|
+
*
|
|
69
|
+
* So what this buys is the expensive half rather than all of it: the rows
|
|
70
|
+
* outside the window are not laid out and not painted, which is where the time
|
|
71
|
+
* goes in a long list, and the node count stays. That is worth having and it is
|
|
72
|
+
* not worth calling something it is not. An element that windows children it
|
|
73
|
+
* was handed is not a virtual list, whatever it is named.
|
|
74
|
+
*
|
|
75
|
+
* **One estimate, not per-row measurement.** Measuring every row to place it is
|
|
76
|
+
* how a virtual list ends up reading the layout it is trying to avoid, and it
|
|
77
|
+
* is what makes them jump: a row taller than its estimate shifts everything
|
|
78
|
+
* after it at the moment it renders. A fixed estimate is a straight line
|
|
79
|
+
* between scroll offset and index, which never disagrees with itself. Rows of
|
|
80
|
+
* genuinely different heights are a different component and a harder one.
|
|
81
|
+
*/
|
|
82
|
+
export class VirtualizeController {
|
|
83
|
+
constructor(host, options) {
|
|
84
|
+
this.watching = false;
|
|
85
|
+
/** Consecutive measurements that moved the window with nothing outside asking. */
|
|
86
|
+
this.settling = 0;
|
|
87
|
+
/** Whether the consumer has been told their box cannot be measured. */
|
|
88
|
+
this.warned = false;
|
|
89
|
+
/** The slice to render, recomputed on scroll and on resize. */
|
|
90
|
+
this.window = { after: 0, before: 0, first: 0, last: 0 };
|
|
91
|
+
/* `passive`, because this never calls `preventDefault` and saying so lets the
|
|
92
|
+
browser scroll without waiting to find out. */
|
|
93
|
+
this.handleScroll = () => {
|
|
94
|
+
// A reader moved, which is the one input this exists to answer.
|
|
95
|
+
this.settling = 0;
|
|
96
|
+
this.measure();
|
|
97
|
+
};
|
|
98
|
+
this.host = host;
|
|
99
|
+
this.options = options;
|
|
100
|
+
host.addController(this);
|
|
101
|
+
}
|
|
102
|
+
hostConnected() {
|
|
103
|
+
/* Nothing is observed here, and that is deliberate. `hostConnected` runs
|
|
104
|
+
before the first render, so the container query returns null and an
|
|
105
|
+
observer attached to nothing observes nothing for the life of the
|
|
106
|
+
element. `CollapseController` shipped exactly that bug and it was
|
|
107
|
+
invisible: the first measurement was right, so the only symptom was a
|
|
108
|
+
window that never moved again. */
|
|
109
|
+
/* **The count is deliberately not cleared here, and clearing it was the
|
|
110
|
+
first attempt.** A resize looks like an outside cause and is not one when
|
|
111
|
+
the box has no height of its own: the render this controller asked for is
|
|
112
|
+
what changed the size, so the observer fires on every pass and resets the
|
|
113
|
+
very counter meant to notice that. Measured: the guard made no difference
|
|
114
|
+
at all, 7776 passes in twenty-five seconds, identical to having none.
|
|
115
|
+
|
|
116
|
+
Nothing is lost by leaving it. A genuine resize changes the window once
|
|
117
|
+
and the next measurement agrees, which clears the count through `same`. A
|
|
118
|
+
runaway never agrees, which is the whole distinction. */
|
|
119
|
+
this.observer = new ResizeObserver(() => {
|
|
120
|
+
this.measure();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
hostDisconnected() {
|
|
124
|
+
this.observer?.disconnect();
|
|
125
|
+
this.observer = undefined;
|
|
126
|
+
this.watching = false;
|
|
127
|
+
this.options.container()?.removeEventListener('scroll', this.handleScroll);
|
|
128
|
+
}
|
|
129
|
+
/** Attaches to the box the first time there is one to attach to. */
|
|
130
|
+
watch() {
|
|
131
|
+
if (this.watching) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const box = this.options.container();
|
|
135
|
+
if (!box || !this.observer) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.observer.observe(box);
|
|
139
|
+
box.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
140
|
+
this.watching = true;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Reads the scroll offset and settles the window.
|
|
144
|
+
*
|
|
145
|
+
* Asks for an update only when the slice actually moved. A scroll fires many
|
|
146
|
+
* events per frame and most of them land inside the row already at the top,
|
|
147
|
+
* so requesting one per event would re-render the list for every pixel.
|
|
148
|
+
*/
|
|
149
|
+
measure() {
|
|
150
|
+
this.watch();
|
|
151
|
+
const box = this.options.container();
|
|
152
|
+
const total = Math.max(0, this.options.count());
|
|
153
|
+
const size = Math.max(1, this.options.estimate());
|
|
154
|
+
if (!box) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const overscan = this.options.overscan ?? 4;
|
|
158
|
+
const first = Math.max(0, Math.floor(box.scrollTop / size) - overscan);
|
|
159
|
+
const fits = Math.ceil(box.clientHeight / size) + overscan * 2;
|
|
160
|
+
const last = Math.min(total, first + fits);
|
|
161
|
+
const next = {
|
|
162
|
+
after: Math.max(0, (total - last) * size),
|
|
163
|
+
before: first * size,
|
|
164
|
+
first,
|
|
165
|
+
last,
|
|
166
|
+
};
|
|
167
|
+
/* The host's own numbers changing is an outside cause, the same as a scroll
|
|
168
|
+
or a resize: a set that grew is not this controller chasing itself. */
|
|
169
|
+
if (this.last?.count !== total || this.last.estimate !== size) {
|
|
170
|
+
this.last = { count: total, estimate: size };
|
|
171
|
+
this.settling = 0;
|
|
172
|
+
}
|
|
173
|
+
const same = next.first === this.window.first &&
|
|
174
|
+
next.last === this.window.last &&
|
|
175
|
+
next.before === this.window.before &&
|
|
176
|
+
next.after === this.window.after;
|
|
177
|
+
if (same) {
|
|
178
|
+
// It settled, so nothing that came before it was a runaway either.
|
|
179
|
+
this.settling = 0;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (this.settling >= UNSTABLE) {
|
|
183
|
+
/* Hold what is *rendered*, which is `this.window`, rather than adopting a
|
|
184
|
+
reading nothing will draw. Refusing the update and keeping `next` would
|
|
185
|
+
leave the two disagreeing with no way back: the next measurement would
|
|
186
|
+
find them the same and do nothing. */
|
|
187
|
+
this.report();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.settling += 1;
|
|
191
|
+
this.window = next;
|
|
192
|
+
this.host.requestUpdate();
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Says once that the box cannot be measured, and why.
|
|
196
|
+
*
|
|
197
|
+
* A warning rather than a throw, because the list still works: it holds a
|
|
198
|
+
* window, it draws rows, and a reader can scroll. What it cannot do is choose
|
|
199
|
+
* the window well, and that is the consumer's stylesheet to fix, not
|
|
200
|
+
* something this can decide on their behalf.
|
|
201
|
+
*
|
|
202
|
+
* Once per controller, because a warning per frame is one people filter out
|
|
203
|
+
* and this one has to be believed the day it is right.
|
|
204
|
+
*/
|
|
205
|
+
report() {
|
|
206
|
+
if (this.warned) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.warned = true;
|
|
210
|
+
console.warn(`VirtualizeController: the window did not settle in ${UNSTABLE} passes, so it is holding ` +
|
|
211
|
+
'the last one. That happens when the scrolling box has no height of its own: its ' +
|
|
212
|
+
'`clientHeight` is then a function of the rows just drawn, so measuring moves what is ' +
|
|
213
|
+
'being measured and the two never agree. Give the container a block size and an ' +
|
|
214
|
+
'overflow, which is what the styled tier does and what a headless consumer has to.');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Records the events a set of tags dispatch. Called by generated code. */
|
|
2
|
+
export declare const registerElementEvents: (entries: Readonly<Record<string, readonly string[]>>) => void;
|
|
3
|
+
/** The events a tag dispatches, or `undefined` if nothing registered it. */
|
|
4
|
+
export declare const eventsForTag: (tag: string) => ReadonlySet<string> | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which events each tag actually dispatches, for the React prop audit.
|
|
3
|
+
*
|
|
4
|
+
* **The audit could already tell a name from a typo and not a name from a
|
|
5
|
+
* misplacement.** `helixEventNames` is every event the library has, so
|
|
6
|
+
* `onhelix-resize` written on an element that never fires it read as valid: the
|
|
7
|
+
* name is real, and nothing knew that this element is not the one that sends it.
|
|
8
|
+
* That is the likelier mistake of the two, because it happens while writing a
|
|
9
|
+
* *correct* event name.
|
|
10
|
+
*
|
|
11
|
+
* Measured 25 Aug 2026: `onhelix-resize` was written on `<helix-sandbox>` where
|
|
12
|
+
* the dispatcher is the `<helix-sandbox-preview>` inside it. Nothing warned, the
|
|
13
|
+
* handler never ran, and the page it was sizing sat at its floor looking exactly
|
|
14
|
+
* like the fixed layout it replaced.
|
|
15
|
+
*
|
|
16
|
+
* **Registered rather than imported, because the direction of the tiers forbids
|
|
17
|
+
* the other way round.** This lives in `core`, which is below the packages that
|
|
18
|
+
* define elements and cannot name them. Each package ships a generated call
|
|
19
|
+
* instead, so the map is filled by whoever is loaded and a consumer importing
|
|
20
|
+
* one component is checked against that component.
|
|
21
|
+
*
|
|
22
|
+
* A tag nobody registered is not an error. It is a component from outside this
|
|
23
|
+
* library, or a package built before the generator existed, and the audit says
|
|
24
|
+
* nothing rather than guessing.
|
|
25
|
+
*/
|
|
26
|
+
const declared = new Map();
|
|
27
|
+
/** Records the events a set of tags dispatch. Called by generated code. */
|
|
28
|
+
export const registerElementEvents = (entries) => {
|
|
29
|
+
for (const [tag, names] of Object.entries(entries)) {
|
|
30
|
+
declared.set(tag, new Set(names));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** The events a tag dispatches, or `undefined` if nothing registered it. */
|
|
34
|
+
export const eventsForTag = (tag) => declared.get(tag);
|