@fusion.dev/helix-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +4 -4
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +0 -105
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +0 -71
- package/dist/controllers/command-controller/command-controller.js +0 -62
- package/dist/controllers/copy-controller/copy-controller.js +0 -63
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +0 -40
- package/dist/controllers/dismiss-controller/dismiss-controller.js +0 -124
- package/dist/controllers/drilldown-controller/drilldown-controller.js +0 -87
- package/dist/controllers/floating-controller/floating-controller.js +0 -152
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +0 -35
- package/dist/controllers/indicator-controller/indicator-controller.js +0 -82
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +0 -46
- package/dist/controllers/popover-controller/popover-controller.js +0 -72
- package/dist/controllers/source-controller/source-controller.js +0 -38
- package/dist/controllers/token-color-controller/token-color-controller.js +0 -51
- package/dist/controllers/virtualize-controller/virtualize-controller.js +0 -128
- package/dist/events/element-events.js +0 -22
- package/dist/events/events.js +0 -386
- package/dist/helix-element/helix-element.d.ts +10 -17
- package/dist/helix-element/helix-element.js +0 -505
- package/dist/helix-form-element/helix-form-element.js +0 -140
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +0 -77
- package/dist/mixins/mixin-collection/mixin-collection.js +0 -83
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +0 -82
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +0 -78
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +0 -93
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +0 -87
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +0 -38
- package/dist/mixins/mixin-field/mixin-field.js +0 -277
- package/dist/utils/anchor-element/anchor-element.js +0 -23
- package/dist/utils/capture-pointer/capture-pointer.js +0 -18
- package/dist/utils/closest-instance/closest-instance.js +0 -20
- package/dist/utils/create-sheet/create-sheet.js +0 -14
- package/dist/utils/custom-element/custom-element.js +0 -83
- package/dist/utils/draw-icon/draw-icon.js +0 -28
- package/dist/utils/draw-tooltip/draw-tooltip.js +1 -50
- package/dist/utils/elapsed-time/elapsed-time.js +0 -51
- package/dist/utils/focusable-elements/focusable-elements.js +1 -59
- package/dist/utils/format-numbers/format-numbers.js +0 -16
- package/dist/utils/host-aria/host-aria.js +0 -26
- package/dist/utils/inherit-disabled/inherit-disabled.js +0 -36
- package/dist/utils/line-path/line-path.js +0 -36
- package/dist/utils/on-unless-false/on-unless-false.js +0 -23
- package/dist/utils/read-series/read-series.js +0 -71
- package/dist/utils/relative-time/relative-time.js +0 -43
- package/dist/utils/resolve-active-item/resolve-active-item.js +0 -20
- package/dist/utils/restore-value/restore-value.js +0 -54
- package/dist/utils/scale/scale.js +0 -19
- package/dist/utils/scale/ticks.js +0 -36
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +0 -53
- package/dist/utils/slot-filled/slot-filled.js +0 -23
- package/dist/utils/tell-disclosure/tell-disclosure.js +0 -53
- package/dist/utils/ticker/ticker.js +0 -22
- package/dist/utils/tone-marks/tone-marks.js +0 -24
- package/dist/utils/value-as/value-as.js +0 -56
- package/package.json +5 -5
package/custom-elements.json
CHANGED
|
@@ -2924,7 +2924,7 @@
|
|
|
2924
2924
|
}
|
|
2925
2925
|
}
|
|
2926
2926
|
],
|
|
2927
|
-
"description": "Records a boolean fact about this element as a custom state.\n\n**The host belongs to whoever rendered it, and a component's own\nbookkeeping may not be kept there.** An attribute written on the first\nupdate is markup a consumer's server never sent, so a React consumer\nhydrates a DOM that has gained attributes since and reports a mismatch it\nwill not patch up. A custom state is not in the DOM at all: it carries the\nfact to a stylesheet through `:state()` and leaves the markup alone, so the\nserver's output and the client's agree by construction.\n\n`SETTLED` carries the long version of that argument; `checked`, `open`,\n`invalid` and `has-label` are recorded the same way, each of them a state a\n`default-*` attribute resolves before React reaches the node.\n\n**Deferring the definitions is not a fix and could not be.** Importing them\nfrom an effect so the upgrades fall the other side of hydration works only\nwhile hydration is one pass: anything that splits it, a streamed segment or\na suspended boundary, puts an upgrade back inside the window. Timing cannot\nbe the guarantee when the fact is kept somewhere React reconciles.\n\nFeature-tested rather than assumed, the way `hostAria` tests element\nreferences. An engine without `CustomStateSet` records nothing and keeps\nwhatever the resting rule draws, which is the safe direction to fail.\n\n**
|
|
2927
|
+
"description": "Records a boolean fact about this element as a custom state.\n\n**The host belongs to whoever rendered it, and a component's own\nbookkeeping may not be kept there.** An attribute written on the first\nupdate is markup a consumer's server never sent, so a React consumer\nhydrates a DOM that has gained attributes since and reports a mismatch it\nwill not patch up. A custom state is not in the DOM at all: it carries the\nfact to a stylesheet through `:state()` and leaves the markup alone, so the\nserver's output and the client's agree by construction.\n\n`SETTLED` carries the long version of that argument; `checked`, `open`,\n`invalid` and `has-label` are recorded the same way, each of them a state a\n`default-*` attribute resolves before React reaches the node.\n\n**Deferring the definitions is not a fix and could not be.** Importing them\nfrom an effect so the upgrades fall the other side of hydration works only\nwhile hydration is one pass: anything that splits it, a streamed segment or\na suspended boundary, puts an upgrade back inside the window. Timing cannot\nbe the guarantee when the fact is kept somewhere React reconciles.\n\nFeature-tested rather than assumed, the way `hostAria` tests element\nreferences. An engine without `CustomStateSet` records nothing and keeps\nwhatever the resting rule draws, which is the safe direction to fail.\n\n**The engines the test is for are browsers**: Safari below 17.4 and Firefox\nbelow 126, where the write is a `TypeError`, so a component bypassing this\nfails to render rather than rendering without whatever the state draws.\nLit's server shim sets `states`, so the guard passes there and records a\nstate nothing reads, which is harmless."
|
|
2928
2928
|
},
|
|
2929
2929
|
{
|
|
2930
2930
|
"kind": "field",
|
|
@@ -2933,7 +2933,7 @@
|
|
|
2933
2933
|
"text": "Promise<void>"
|
|
2934
2934
|
},
|
|
2935
2935
|
"readonly": true,
|
|
2936
|
-
"description": "Resolves a frame after the first update, when transitions are switched on.\n\n**Named `whenSettled` because `settled`
|
|
2936
|
+
"description": "Resolves a frame after the first update, when transitions are switched on.\n\n**Named `whenSettled` because `settled` is taken four times over**, by\nImage, Splitter and both color controls, none of them about transitions. A\nbase class takes the name every subclass then cannot use.\n\n**The state it reports cannot be watched, which is why this exists.** A\ncustom state is deliberately not in the DOM, so there is no\n`MutationObserver` equivalent for `ElementInternals.states`. Await this\ninstead.\n\nSeparate from `updateComplete`, which resolves in the microtask checkpoint\nthe suppression is still active in. That is the distinction `readyToMove`\nin `@fusion.dev/helix-testing` is written around.\n\nIt never resolves where `requestAnimationFrame` never fires, which is a\nserver, a hidden tab, and happy-dom. Callers that must not hang race it\nagainst a timeout; `readyToMove` does exactly that.",
|
|
2937
2937
|
"default": "new Promise<void>((resolve) => { this.resolveSettled = resolve })"
|
|
2938
2938
|
},
|
|
2939
2939
|
{
|
|
@@ -3204,7 +3204,7 @@
|
|
|
3204
3204
|
}
|
|
3205
3205
|
}
|
|
3206
3206
|
],
|
|
3207
|
-
"description": "Records a boolean fact about this element as a custom state.\n\n**The host belongs to whoever rendered it, and a component's own\nbookkeeping may not be kept there.** An attribute written on the first\nupdate is markup a consumer's server never sent, so a React consumer\nhydrates a DOM that has gained attributes since and reports a mismatch it\nwill not patch up. A custom state is not in the DOM at all: it carries the\nfact to a stylesheet through `:state()` and leaves the markup alone, so the\nserver's output and the client's agree by construction.\n\n`SETTLED` carries the long version of that argument; `checked`, `open`,\n`invalid` and `has-label` are recorded the same way, each of them a state a\n`default-*` attribute resolves before React reaches the node.\n\n**Deferring the definitions is not a fix and could not be.** Importing them\nfrom an effect so the upgrades fall the other side of hydration works only\nwhile hydration is one pass: anything that splits it, a streamed segment or\na suspended boundary, puts an upgrade back inside the window. Timing cannot\nbe the guarantee when the fact is kept somewhere React reconciles.\n\nFeature-tested rather than assumed, the way `hostAria` tests element\nreferences. An engine without `CustomStateSet` records nothing and keeps\nwhatever the resting rule draws, which is the safe direction to fail.\n\n**
|
|
3207
|
+
"description": "Records a boolean fact about this element as a custom state.\n\n**The host belongs to whoever rendered it, and a component's own\nbookkeeping may not be kept there.** An attribute written on the first\nupdate is markup a consumer's server never sent, so a React consumer\nhydrates a DOM that has gained attributes since and reports a mismatch it\nwill not patch up. A custom state is not in the DOM at all: it carries the\nfact to a stylesheet through `:state()` and leaves the markup alone, so the\nserver's output and the client's agree by construction.\n\n`SETTLED` carries the long version of that argument; `checked`, `open`,\n`invalid` and `has-label` are recorded the same way, each of them a state a\n`default-*` attribute resolves before React reaches the node.\n\n**Deferring the definitions is not a fix and could not be.** Importing them\nfrom an effect so the upgrades fall the other side of hydration works only\nwhile hydration is one pass: anything that splits it, a streamed segment or\na suspended boundary, puts an upgrade back inside the window. Timing cannot\nbe the guarantee when the fact is kept somewhere React reconciles.\n\nFeature-tested rather than assumed, the way `hostAria` tests element\nreferences. An engine without `CustomStateSet` records nothing and keeps\nwhatever the resting rule draws, which is the safe direction to fail.\n\n**The engines the test is for are browsers**: Safari below 17.4 and Firefox\nbelow 126, where the write is a `TypeError`, so a component bypassing this\nfails to render rather than rendering without whatever the state draws.\nLit's server shim sets `states`, so the guard passes there and records a\nstate nothing reads, which is harmless.",
|
|
3208
3208
|
"inheritedFrom": {
|
|
3209
3209
|
"name": "HelixElement",
|
|
3210
3210
|
"module": "src/helix-element/helix-element.ts"
|
|
@@ -3217,7 +3217,7 @@
|
|
|
3217
3217
|
"text": "Promise<void>"
|
|
3218
3218
|
},
|
|
3219
3219
|
"readonly": true,
|
|
3220
|
-
"description": "Resolves a frame after the first update, when transitions are switched on.\n\n**Named `whenSettled` because `settled`
|
|
3220
|
+
"description": "Resolves a frame after the first update, when transitions are switched on.\n\n**Named `whenSettled` because `settled` is taken four times over**, by\nImage, Splitter and both color controls, none of them about transitions. A\nbase class takes the name every subclass then cannot use.\n\n**The state it reports cannot be watched, which is why this exists.** A\ncustom state is deliberately not in the DOM, so there is no\n`MutationObserver` equivalent for `ElementInternals.states`. Await this\ninstead.\n\nSeparate from `updateComplete`, which resolves in the microtask checkpoint\nthe suppression is still active in. That is the distinction `readyToMove`\nin `@fusion.dev/helix-testing` is written around.\n\nIt never resolves where `requestAnimationFrame` never fires, which is a\nserver, a hidden tab, and happy-dom. Callers that must not hang race it\nagainst a timeout; `readyToMove` does exactly that.",
|
|
3221
3221
|
"default": "new Promise<void>((resolve) => { this.resolveSettled = resolve })",
|
|
3222
3222
|
"inheritedFrom": {
|
|
3223
3223
|
"name": "HelixElement",
|
|
@@ -1,110 +1,20 @@
|
|
|
1
|
-
/** The two relationships a consumer writes on a host and means for the control. */
|
|
2
1
|
const RELATIONSHIPS = [
|
|
3
2
|
['aria-describedby', 'ariaDescribedByElements'],
|
|
4
3
|
['aria-labelledby', 'ariaLabelledByElements'],
|
|
5
4
|
];
|
|
6
|
-
/**
|
|
7
|
-
* The state attributes a host may carry and mean for the control inside it.
|
|
8
|
-
*
|
|
9
|
-
* **Copied rather than resolved, which is what makes them a separate list.** A
|
|
10
|
-
* relationship above names ids in the consumer's tree and can only travel as an
|
|
11
|
-
* element reference; these are plain strings and the value means the same thing
|
|
12
|
-
* wherever it is written, so the whole of the work is putting it on the node the
|
|
13
|
-
* accessibility tree actually exposes.
|
|
14
|
-
*
|
|
15
|
-
* **A state written on a host reaches nobody.** `Megamenu` and `SpeedDial` write
|
|
16
|
-
* `aria-expanded` onto whatever a consumer slots, and where that is a
|
|
17
|
-
* `<helix-button>` it lands on a host the tree reports as `generic`: measured
|
|
18
|
-
* through Chrome's own tree, a button named "Mega" with no expanded state at
|
|
19
|
-
* all. A plain `<button slot="trigger">` never had the problem, because there
|
|
20
|
-
* the host *is* the focusable node.
|
|
21
|
-
*
|
|
22
|
-
* **Only what this controller put there is ever taken away.** Removing any of
|
|
23
|
-
* these from the control whenever the host lacks it reads as the obvious way to
|
|
24
|
-
* keep the two in step and quietly deletes what the component renders for
|
|
25
|
-
* itself: `Combobox` writes `aria-controls="listbox"` onto its own input and
|
|
26
|
-
* `Select` writes `aria-expanded` onto its own trigger, and a controller
|
|
27
|
-
* carrying nothing would wipe both on every update.
|
|
28
|
-
*
|
|
29
|
-
* So the record below holds what was forwarded last time, and a host that stops
|
|
30
|
-
* carrying one removes exactly that and nothing else. A control's own attribute
|
|
31
|
-
* is untouched either way, which is the only rule that can be right: this
|
|
32
|
-
* controller knows what the host said and cannot know what the template meant.
|
|
33
|
-
*/
|
|
34
5
|
const STATES = ['aria-expanded', 'aria-haspopup', 'aria-controls'];
|
|
35
|
-
/**
|
|
36
|
-
* Carries a host's ARIA references down to the control inside it.
|
|
37
|
-
*
|
|
38
|
-
* **The gap this closes, measured.** `<helix-button aria-describedby="tip">` puts
|
|
39
|
-
* the description on the host, and the host is not what a reader is on: the
|
|
40
|
-
* accessibility tree exposes the `<button>` in the shadow root, and the host
|
|
41
|
-
* beside it is a generic with a description nobody hears. Read from Chrome's own
|
|
42
|
-
* tree:
|
|
43
|
-
*
|
|
44
|
-
* ```
|
|
45
|
-
* host role=generic name="" desc="Helix description"
|
|
46
|
-
* inner button role=button name="Helix" desc=""
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* A plain `<button>` with the identical attribute reads `desc="Helix
|
|
50
|
-
* description"`, so the reference itself is fine and only its destination is
|
|
51
|
-
* wrong. This moves it.
|
|
52
|
-
*
|
|
53
|
-
* **An IDREF cannot make that trip and an element reference can.** The ids are
|
|
54
|
-
* written in the consumer's tree and the control is in a shadow root, so
|
|
55
|
-
* `aria-describedby` copied onto the inner element resolves to nothing.
|
|
56
|
-
* `ariaDescribedByElements` takes the elements themselves, and a reference is
|
|
57
|
-
* valid into the same tree *or a shadow-including ancestor* of it: from inside
|
|
58
|
-
* the shadow root looking out, the document is exactly that. Measured, the
|
|
59
|
-
* assignment reads back with the node intact, where the inward direction, from a
|
|
60
|
-
* light-DOM trigger to a node in a shadow root, reads back empty. This is the
|
|
61
|
-
* one direction that works, which is why `Tooltip` needs `for` and this does
|
|
62
|
-
* not.
|
|
63
|
-
*
|
|
64
|
-
* **The attributes stay on the host.** Nothing is removed and nothing is added:
|
|
65
|
-
* the consumer's markup is exactly what they wrote, which is the same reason
|
|
66
|
-
* `hostAria` exists. What changes is only that the control now also holds the
|
|
67
|
-
* reference, so a screen reader on the focused element hears it.
|
|
68
|
-
*
|
|
69
|
-
* Watched with a `MutationObserver`, because a reference can arrive at any time:
|
|
70
|
-
* a framework rendering a tooltip after this element upgrades, or a consumer
|
|
71
|
-
* pointing `aria-describedby` at a different id, changes an attribute and
|
|
72
|
-
* nothing schedules a Lit update.
|
|
73
|
-
*/
|
|
74
6
|
export class AriaForwardController {
|
|
75
7
|
constructor(host, options) {
|
|
76
|
-
/** Which state attributes this controller wrote, so only those are removed. */
|
|
77
8
|
this.forwarded = new Set();
|
|
78
9
|
this.host = host;
|
|
79
10
|
this.target = options.target;
|
|
80
11
|
host.addController(this);
|
|
81
12
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Resolves the host's references and hands them to the control.
|
|
84
|
-
*
|
|
85
|
-
* Resolved through the host's own root rather than `document`, so a button
|
|
86
|
-
* inside another component's shadow root finds the ids written beside it
|
|
87
|
-
* there. Ids that resolve to nothing are dropped rather than passed on: an
|
|
88
|
-
* `aria-describedby` naming a heading that has been removed is a stale
|
|
89
|
-
* reference, and an empty array clears the property rather than leaving the
|
|
90
|
-
* last good one in place.
|
|
91
|
-
*/
|
|
92
13
|
forward() {
|
|
93
14
|
const target = this.target();
|
|
94
15
|
if (!target) {
|
|
95
16
|
return;
|
|
96
17
|
}
|
|
97
|
-
/* **The record belongs to the element it was written about.** `forwarded`
|
|
98
|
-
says what this controller put on the control. Kept across a replacement,
|
|
99
|
-
it claims a value the new element never had, and the clearing branches
|
|
100
|
-
below then take off what the component wrote itself: the fault the record
|
|
101
|
-
exists to prevent, one element later.
|
|
102
|
-
|
|
103
|
-
**No component in this library replaces its control**, which is worth
|
|
104
|
-
saying because the comment here used to name one. All fifteen consumers
|
|
105
|
-
resolve to a single fixed part rendered once. `target` is a callback and
|
|
106
|
-
its contract permits a swap, so a controller in `core` is held to the
|
|
107
|
-
contract rather than to the fifteen callers it happens to have today. */
|
|
108
18
|
if (target !== this.recorded) {
|
|
109
19
|
this.forwarded.clear();
|
|
110
20
|
this.recorded = target;
|
|
@@ -112,9 +22,6 @@ export class AriaForwardController {
|
|
|
112
22
|
const root = this.host.getRootNode();
|
|
113
23
|
for (const [attribute, property] of RELATIONSHIPS) {
|
|
114
24
|
if (!(property in target)) {
|
|
115
|
-
// An engine without element reflection. Nothing to do, and nothing that
|
|
116
|
-
// silently half works: assigning to a property it does not implement is
|
|
117
|
-
// a no-op rather than an error, which is what the guard makes visible.
|
|
118
25
|
continue;
|
|
119
26
|
}
|
|
120
27
|
const ids = (this.host.getAttribute(attribute) ?? '').split(/\s+/).filter(Boolean);
|
|
@@ -127,14 +34,6 @@ export class AriaForwardController {
|
|
|
127
34
|
this.forwarded.add(attribute);
|
|
128
35
|
continue;
|
|
129
36
|
}
|
|
130
|
-
/* Only if this controller is what put it there, which is the same rule
|
|
131
|
-
the states below keep and for a worse reason. `MixinField` writes
|
|
132
|
-
`aria-describedby` on the control for its own hint, and assigning null
|
|
133
|
-
here removes the content attribute, so forwarding nothing wiped the
|
|
134
|
-
component's own description on all eighteen form controls. Invisible to
|
|
135
|
-
every DOM spec: happy-dom implements no ARIA element reflection, so the
|
|
136
|
-
`property in target` guard above skips this whole branch and the suite
|
|
137
|
-
is green against code that clears a description in every browser. */
|
|
138
37
|
if (this.forwarded.has(attribute)) {
|
|
139
38
|
reflect[property] = null;
|
|
140
39
|
this.forwarded.delete(attribute);
|
|
@@ -143,7 +42,6 @@ export class AriaForwardController {
|
|
|
143
42
|
for (const attribute of STATES) {
|
|
144
43
|
const value = this.host.getAttribute(attribute);
|
|
145
44
|
if (value === null) {
|
|
146
|
-
// Only if this controller is what put it there. See the note above.
|
|
147
45
|
if (this.forwarded.has(attribute)) {
|
|
148
46
|
target.removeAttribute(attribute);
|
|
149
47
|
this.forwarded.delete(attribute);
|
|
@@ -164,9 +62,6 @@ export class AriaForwardController {
|
|
|
164
62
|
this.observer?.disconnect();
|
|
165
63
|
this.observer = undefined;
|
|
166
64
|
}
|
|
167
|
-
/* After the render that created the control, because there is nothing to
|
|
168
|
-
forward to before it exists. Every update, not only the first: a component
|
|
169
|
-
that re-renders its control produces a new element with none of this. */
|
|
170
65
|
hostUpdated() {
|
|
171
66
|
this.forward();
|
|
172
67
|
}
|
|
@@ -1,51 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which reading a pointer is nearest, for a chart that draws no markers.
|
|
3
|
-
*
|
|
4
|
-
* **The anchor is a position and not an element, and that is the whole reason
|
|
5
|
-
* this exists.** A bar and a donut slice are elements a tooltip could be hung
|
|
6
|
-
* on; a line is one `<path>` for a whole series, because
|
|
7
|
-
* `preserveAspectRatio="none"` turns a circle into an ellipse and there is no
|
|
8
|
-
* vector-effect that answers for a shape, so `LineChart` deliberately draws no
|
|
9
|
-
* per-reading marker. Anchoring per datum works for two charts of four and
|
|
10
|
-
* cannot be extended to the third, so the resolution is arithmetic instead.
|
|
11
|
-
*
|
|
12
|
-
* **It maps along one axis only.** The nearest reading to a pointer is decided
|
|
13
|
-
* by x alone in a vertical chart, whatever y is doing, because a reader moving
|
|
14
|
-
* across a chart is asking about a column of it rather than about a point. A
|
|
15
|
-
* true nearest-point search would make a two-series chart answer differently
|
|
16
|
-
* depending on which line the pointer drifted toward, which is a tooltip that
|
|
17
|
-
* changes its mind while the reader holds still.
|
|
18
|
-
*
|
|
19
|
-
* **Coarse pointers get nothing.** A touch has no hover, so a tooltip driven by
|
|
20
|
-
* one appears where a finger already covers it and stays after the finger
|
|
21
|
-
* lifts. The table underneath is the readable version and is what a phone
|
|
22
|
-
* should be given, so this listens for `pointermove` and ignores anything whose
|
|
23
|
-
* `pointerType` is not fine.
|
|
24
|
-
*/
|
|
25
1
|
export class ChartPointerController {
|
|
26
2
|
#host;
|
|
27
3
|
#moved;
|
|
28
4
|
#plot;
|
|
29
5
|
constructor(host) {
|
|
30
6
|
this.#moved = false;
|
|
31
|
-
/** The reading under the pointer, or -1 when the pointer is elsewhere. */
|
|
32
7
|
this.at = -1;
|
|
33
|
-
/**
|
|
34
|
-
* Which axis the readings are laid out along.
|
|
35
|
-
*
|
|
36
|
-
* **An axis and not a boolean, because two of the four charts do not use x.**
|
|
37
|
-
* A horizontal bar chart runs its categories *down* the box, so resolving
|
|
38
|
-
* every reading from the pointer's x leaves a move from the first row to the
|
|
39
|
-
* last never changing the answer: measured on four rows, all four reported
|
|
40
|
-
* the third, which is the slot the middle of the box falls in. It looks like
|
|
41
|
-
* a tooltip that will not follow and is a tooltip reading the wrong axis.
|
|
42
|
-
*
|
|
43
|
-
* `none` is the ring. Its slices are real elements at every angle, so the
|
|
44
|
-
* reading under the pointer is the one the pointer is over, and arithmetic on
|
|
45
|
-
* either axis would answer with whichever slice shared a coordinate: at three
|
|
46
|
-
* o'clock and at nine o'clock, x alone gives the same slice for opposite
|
|
47
|
-
* sides. The slices call `set` and this only holds the answer.
|
|
48
|
-
*/
|
|
49
8
|
this.axis = 'inline';
|
|
50
9
|
this.#move = (event) => {
|
|
51
10
|
if (this.axis === 'none' || (event.pointerType !== 'mouse' && event.pointerType !== 'pen')) {
|
|
@@ -57,11 +16,6 @@ export class ChartPointerController {
|
|
|
57
16
|
if (!box || !along || count === 0) {
|
|
58
17
|
return;
|
|
59
18
|
}
|
|
60
|
-
/* The same slot arithmetic the charts draw with: reading `i` sits in the
|
|
61
|
-
middle of the `i`th of `count` equal slots, so the nearest is the slot
|
|
62
|
-
the pointer is in. Rounding a proportion of `count - 1` instead puts the
|
|
63
|
-
boundaries half a slot off and the first and last reading get half the
|
|
64
|
-
catchment of the others. */
|
|
65
19
|
const from = this.axis === 'block' ? event.clientY - box.top : event.clientX - box.left;
|
|
66
20
|
const slot = Math.floor((from / along) * count);
|
|
67
21
|
const next = Math.min(Math.max(slot, 0), count - 1);
|
|
@@ -89,20 +43,6 @@ export class ChartPointerController {
|
|
|
89
43
|
this.#host.removeEventListener('pointerleave', this.#leave);
|
|
90
44
|
this.at = -1;
|
|
91
45
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Tells the tooltip its anchor moved, which nothing else would.
|
|
94
|
-
*
|
|
95
|
-
* The mark the tooltip is hung on is one element the chart walks from reading
|
|
96
|
-
* to reading with an inline style, and Floating UI's `autoUpdate` watches
|
|
97
|
-
* scrolling, resizing and layout shift. Moving an absolutely positioned
|
|
98
|
-
* element by writing `inset` is none of the three, so the bubble is placed
|
|
99
|
-
* once on open and then stays where the first reading was while the mark
|
|
100
|
-
* travels out from under it.
|
|
101
|
-
*
|
|
102
|
-
* Here rather than in the chart because the reading is this controller's, and
|
|
103
|
-
* after the update rather than in `set` because the mark is still at the old
|
|
104
|
-
* position until the render that follows.
|
|
105
|
-
*/
|
|
106
46
|
hostUpdated() {
|
|
107
47
|
if (!this.#moved) {
|
|
108
48
|
return;
|
|
@@ -112,21 +52,10 @@ export class ChartPointerController {
|
|
|
112
52
|
.querySelector('[part="tooltip-anchor"]')
|
|
113
53
|
?.reposition?.();
|
|
114
54
|
}
|
|
115
|
-
/**
|
|
116
|
-
* The box the readings are laid out in, which is the plot and not the host.
|
|
117
|
-
*
|
|
118
|
-
* The host also holds the row of labels, so measuring against it would map
|
|
119
|
-
* the pointer over a box taller than the drawing. That mattered the moment a
|
|
120
|
-
* chart started measuring down the box rather than across it, which is what
|
|
121
|
-
* `axis` is for: on a horizontal bar chart the labels are beside the plot
|
|
122
|
-
* rather than under it, and the host is wider than the drawing by exactly
|
|
123
|
-
* their column.
|
|
124
|
-
*/
|
|
125
55
|
#box() {
|
|
126
56
|
this.#plot ??= this.#host.renderRoot.querySelector('[part="plot"]') ?? undefined;
|
|
127
57
|
return this.#plot?.getBoundingClientRect();
|
|
128
58
|
}
|
|
129
|
-
/** Puts the pointer on a reading, for a chart that resolves it some other way. */
|
|
130
59
|
set(at) {
|
|
131
60
|
if (at !== this.at) {
|
|
132
61
|
this.at = at;
|
|
@@ -1,73 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether the browser implements invoker commands.
|
|
3
|
-
*
|
|
4
|
-
* Feature-detected on the prototype rather than on an instance, so it can be
|
|
5
|
-
* answered before anything has rendered, and guarded for a server, which has no
|
|
6
|
-
* `HTMLButtonElement` at all.
|
|
7
|
-
*/
|
|
8
1
|
export const supportsInvokers = () => typeof HTMLButtonElement !== 'undefined' && 'commandForElement' in HTMLButtonElement.prototype;
|
|
9
|
-
/**
|
|
10
|
-
* The `command` event, for a browser that does not send one.
|
|
11
|
-
*
|
|
12
|
-
* Invoker commands are recent: Chrome 135, Firefox 137, Safari 18.4. Before
|
|
13
|
-
* this, a button in an older browser set `commandForElement` on nothing, the
|
|
14
|
-
* wiring silently did not happen, and the control did nothing at all when
|
|
15
|
-
* clicked. No error, no warning, and every `command` in the library dead at
|
|
16
|
-
* once, which is the worst way for a feature to be missing: the markup is
|
|
17
|
-
* right, the component is right, and the button is simply inert.
|
|
18
|
-
*
|
|
19
|
-
* So the button sends this instead where the platform will not. It carries the
|
|
20
|
-
* two members `CommandController` reads and dispatches at the target, which is
|
|
21
|
-
* where the platform dispatches too, so nothing downstream can tell the
|
|
22
|
-
* difference.
|
|
23
|
-
*
|
|
24
|
-
* **The built-in commands are not reproduced.** `show-modal`, `close` and
|
|
25
|
-
* `toggle-popover` are defined against native elements and applied by the
|
|
26
|
-
* browser itself, and writing our own would mean reimplementing part of the
|
|
27
|
-
* platform against elements we do not own. Nothing in this library uses them:
|
|
28
|
-
* a custom element only ever answers a `--` command, which is the platform's
|
|
29
|
-
* marker for "the target decides", and the target is ours.
|
|
30
|
-
*/
|
|
31
2
|
export class HelixFallbackCommandEvent extends Event {
|
|
32
3
|
constructor(command, source) {
|
|
33
|
-
// Matching the platform: fired at the invokee, cancelable, and not
|
|
34
|
-
// bubbling. `CommandController` listens on the target itself, so there is
|
|
35
|
-
// nothing above it that a bubble would reach anyway.
|
|
36
4
|
super('command', { cancelable: true });
|
|
37
5
|
this.command = command;
|
|
38
6
|
this.source = source;
|
|
39
7
|
}
|
|
40
8
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Lets a control elsewhere on the page operate this one, declaratively.
|
|
43
|
-
*
|
|
44
|
-
* ```html
|
|
45
|
-
* <helix-button commandfor="confirm" command="--show">Delete…</helix-button>
|
|
46
|
-
* <helix-dialog id="confirm">…</helix-dialog>
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* **The platform's own invoker, not a convention of ours.** `commandfor` and
|
|
50
|
-
* `command` are attributes the browser understands: it resolves the id, wires
|
|
51
|
-
* the two together, and dispatches a `command` event at the target when the
|
|
52
|
-
* trigger is activated. That is a real event with a real source, so it works
|
|
53
|
-
* for a click, for Enter, for Space, and for anything else the platform decides
|
|
54
|
-
* counts as activation later.
|
|
55
|
-
*
|
|
56
|
-
* A `data-` recipe of our own would mean a delegated document listener a
|
|
57
|
-
* consumer cannot see, an id lookup and activation semantics we would write
|
|
58
|
-
* ourselves, and a convention that is ours to document and theirs to learn.
|
|
59
|
-
*
|
|
60
|
-
* **Only custom commands reach a custom element.** The built-in ones
|
|
61
|
-
* (`show-modal`, `close`, `toggle-popover`) are defined against native elements
|
|
62
|
-
* and the browser applies them itself; pointed at a custom element they do
|
|
63
|
-
* nothing. So the names here start with `--`, which is the platform's marker
|
|
64
|
-
* for "the target decides what this means", and the target deciding is exactly
|
|
65
|
-
* what this is.
|
|
66
|
-
*
|
|
67
|
-
* Unrecognized commands are left alone rather than swallowed, so a host that
|
|
68
|
-
* answers `--show` does not quietly absorb a `--print` meant for something
|
|
69
|
-
* else.
|
|
70
|
-
*/
|
|
71
9
|
export class CommandController {
|
|
72
10
|
constructor(host, options) {
|
|
73
11
|
this.handle = (event) => {
|
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
import { HelixCopyEvent } from '../../events/events.js';
|
|
2
|
-
/**
|
|
3
|
-
* Writes to the clipboard, by whichever of the two routes this context allows.
|
|
4
|
-
*
|
|
5
|
-
* The async API first, because it is the one with a future and the only one that
|
|
6
|
-
* reports failure. It is gated in more places than it looks: an insecure origin,
|
|
7
|
-
* a document that is not focused, and a Permissions Policy that defaults to
|
|
8
|
-
* `self`, which a sandboxed iframe without `allow-same-origin` is not.
|
|
9
|
-
*
|
|
10
|
-
* `execCommand` second, deprecated and still the more widely permitted of the
|
|
11
|
-
* two: it is synchronous and rides the user gesture that called it rather than
|
|
12
|
-
* asking for a permission, so it works in a sandboxed frame where the modern one
|
|
13
|
-
* is refused. A textarea rather than the selection, because the text being
|
|
14
|
-
* copied is not on the page: it is a value.
|
|
15
|
-
*
|
|
16
|
-
* `fixed` and `opacity: 0` rather than off-screen, because scrolling to a
|
|
17
|
-
* focused element is what a negative offset causes, and a copy that jumps the
|
|
18
|
-
* page is worse than one that does not happen.
|
|
19
|
-
*/
|
|
20
2
|
const write = async (text) => {
|
|
21
3
|
try {
|
|
22
4
|
await navigator.clipboard.writeText(text);
|
|
23
5
|
return true;
|
|
24
6
|
}
|
|
25
7
|
catch {
|
|
26
|
-
/* Fall through. There is nothing to report yet. */
|
|
27
8
|
}
|
|
28
9
|
const carrier = document.createElement('textarea');
|
|
29
10
|
carrier.value = text;
|
|
@@ -31,9 +12,6 @@ const write = async (text) => {
|
|
|
31
12
|
carrier.setAttribute('aria-hidden', 'true');
|
|
32
13
|
carrier.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
|
|
33
14
|
document.body.append(carrier);
|
|
34
|
-
/* Focused as well as selected. `select()` focuses in a browser and does not
|
|
35
|
-
everywhere, and `execCommand('copy')` copies the *document's* selection, so
|
|
36
|
-
an unfocused textarea copies whatever was selected on the page instead. */
|
|
37
15
|
carrier.focus();
|
|
38
16
|
carrier.select();
|
|
39
17
|
let wrote = false;
|
|
@@ -46,47 +24,13 @@ const write = async (text) => {
|
|
|
46
24
|
carrier.remove();
|
|
47
25
|
return wrote;
|
|
48
26
|
};
|
|
49
|
-
/**
|
|
50
|
-
* Puts text on the clipboard and holds a confirmation for a moment.
|
|
51
|
-
*
|
|
52
|
-
* A controller rather than a component, because copying is a behavior and not a
|
|
53
|
-
* shape. Color's chip is a swatch that copies, and a copy button is a button
|
|
54
|
-
* that copies, and those two have nothing in common to look at: what they share
|
|
55
|
-
* is the write, the confirmation that follows it, the timer that ends it and the
|
|
56
|
-
* event that announces it. Written twice, the second one would get the failure
|
|
57
|
-
* case wrong.
|
|
58
|
-
*
|
|
59
|
-
* **There are two routes and both are tried**, because the modern one is gated
|
|
60
|
-
* in more places than it looks and a control that does nothing is
|
|
61
|
-
* indistinguishable from a broken one. See `write` above.
|
|
62
|
-
*
|
|
63
|
-
* **Where neither route is allowed, the failure is silence.** The confirmation
|
|
64
|
-
* does not appear, which is the honest report: a message about a clipboard is
|
|
65
|
-
* worse than a chip that did not flash, and there is nothing a reader could do
|
|
66
|
-
* about a permission policy anyway.
|
|
67
|
-
*
|
|
68
|
-
* **The event is announced after the write, not before it.** It is not
|
|
69
|
-
* cancelable for the same reason: by the time anything could be said, the text
|
|
70
|
-
* is already on the clipboard, and an event that cannot prevent what it
|
|
71
|
-
* describes should not pretend otherwise.
|
|
72
|
-
*
|
|
73
|
-
* The timer is cleared when the host leaves the document, or a component removed
|
|
74
|
-
* mid-confirmation asks a detached element to re-render.
|
|
75
|
-
*/
|
|
76
27
|
export class CopyController {
|
|
77
28
|
constructor(host, options = {}) {
|
|
78
29
|
this.host = host;
|
|
79
30
|
this.options = options;
|
|
80
|
-
/** Whether the last write succeeded and is still being confirmed. */
|
|
81
31
|
this.copied = false;
|
|
82
32
|
host.addController(this);
|
|
83
33
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Writes to the clipboard, and confirms it if the platform allowed it.
|
|
86
|
-
*
|
|
87
|
-
* Empty text is not copied. An empty clipboard is not a thing a reader ever
|
|
88
|
-
* asks for, and confirming one is worse than doing nothing.
|
|
89
|
-
*/
|
|
90
34
|
async copy(text) {
|
|
91
35
|
if (text === '') {
|
|
92
36
|
return;
|
|
@@ -94,13 +38,6 @@ export class CopyController {
|
|
|
94
38
|
if (!(await write(text))) {
|
|
95
39
|
return;
|
|
96
40
|
}
|
|
97
|
-
/* **The host may have gone while the clipboard was being written.** Writing
|
|
98
|
-
is asynchronous, `hostDisconnected` runs the moment the element leaves,
|
|
99
|
-
and it clears a timer that does not exist yet, so without this the
|
|
100
|
-
confirmation starts a fresh timer on a detached host and calls
|
|
101
|
-
`requestUpdate` on it a second later. Checked here rather than in
|
|
102
|
-
`reset`, because the thing to skip is the whole confirmation and not only
|
|
103
|
-
its clock. */
|
|
104
41
|
if (this.host instanceof HTMLElement && !this.host.isConnected) {
|
|
105
42
|
return;
|
|
106
43
|
}
|
|
@@ -1,44 +1,4 @@
|
|
|
1
1
|
import { inheritDisabled, } from '../../utils/inherit-disabled/inherit-disabled.js';
|
|
2
|
-
/**
|
|
3
|
-
* Pushes a group's `disabled` onto its members, on every update, unasked.
|
|
4
|
-
*
|
|
5
|
-
* **The cascade itself is `inheritDisabled`, and this is what makes it
|
|
6
|
-
* automatic.** Called by hand it runs only when whichever method holds the call
|
|
7
|
-
* runs, so a group whose items changed by some other path leaves them holding
|
|
8
|
-
* the last answer, and a group that forgets the call gets no cascade at all
|
|
9
|
-
* with nothing to say so: the property exists, the attribute reflects, and the
|
|
10
|
-
* members simply ignore it. That is invisible in review and shows up only when
|
|
11
|
-
* someone disables a set.
|
|
12
|
-
*
|
|
13
|
-
* It adds no API. A group already has `disabled`, from `HelixFormElement` or
|
|
14
|
-
* from the consumer; nothing here introduces a property or a method for anyone
|
|
15
|
-
* to call. By this repo's own rule that makes it a controller and not a mixin:
|
|
16
|
-
* it reacts to a lifecycle and nothing else.
|
|
17
|
-
*
|
|
18
|
-
* So a group states its members once, at construction, and never thinks about
|
|
19
|
-
* it again:
|
|
20
|
-
*
|
|
21
|
-
* new DisabledCascadeController(this, { items: () => this.items })
|
|
22
|
-
*
|
|
23
|
-
* **Runs after every render, and on every `slotchange`.** The second half is
|
|
24
|
-
* not an extra: a member arriving in the light DOM changes no property, so Lit
|
|
25
|
-
* schedules nothing, and a render-only cascade leaves a member slotted into an
|
|
26
|
-
* already-disabled group enabled. Measured before it was added, on a radio
|
|
27
|
-
* group holding two disabled members with a third inserted: `110`, and the
|
|
28
|
-
* third was live in a set nobody could use.
|
|
29
|
-
*
|
|
30
|
-
* The listener is on the render root rather than on the host, because
|
|
31
|
-
* `slotchange` bubbles but is not composed: it reaches the shadow root that
|
|
32
|
-
* owns the slot and stops there. On the host it would never fire at all.
|
|
33
|
-
*
|
|
34
|
-
* After render rather than before, because the items are read from the DOM and
|
|
35
|
-
* a newly slotted member is not there until the render that assigns it.
|
|
36
|
-
*
|
|
37
|
-
* Idempotent, so running on every update costs a comparison per member and
|
|
38
|
-
* nothing else: `inheritDisabled` writes only where the answer changed and
|
|
39
|
-
* remembers what each member said for itself, which is what lets a group be
|
|
40
|
-
* re-enabled without freeing a member that was disabled on its own.
|
|
41
|
-
*/
|
|
42
2
|
export class DisabledCascadeController {
|
|
43
3
|
constructor(host, { items }) {
|
|
44
4
|
this.cascade = () => {
|