@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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/custom-elements.json +8005 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
  4. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
  7. package/dist/controllers/command-controller/command-controller.d.ts +83 -0
  8. package/dist/controllers/command-controller/command-controller.js +93 -0
  9. package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
  10. package/dist/controllers/copy-controller/copy-controller.js +128 -0
  11. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
  12. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
  13. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
  14. package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
  15. package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
  16. package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
  17. package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
  18. package/dist/controllers/floating-controller/floating-controller.js +227 -0
  19. package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
  20. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
  21. package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
  22. package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
  23. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
  24. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
  25. package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
  26. package/dist/controllers/popover-controller/popover-controller.js +150 -0
  27. package/dist/controllers/source-controller/source-controller.d.ts +55 -0
  28. package/dist/controllers/source-controller/source-controller.js +94 -0
  29. package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
  30. package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
  31. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
  32. package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
  33. package/dist/events/element-events.d.ts +4 -0
  34. package/dist/events/element-events.js +34 -0
  35. package/dist/events/events.d.ts +743 -0
  36. package/dist/events/events.js +799 -0
  37. package/dist/helix-element/helix-element.d.ts +201 -0
  38. package/dist/helix-element/helix-element.js +695 -0
  39. package/dist/helix-form-element/helix-form-element.d.ts +152 -0
  40. package/dist/helix-form-element/helix-form-element.js +194 -0
  41. package/dist/index.d.ts +54 -0
  42. package/dist/index.js +54 -0
  43. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
  44. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
  45. package/dist/mixins/mixin-collection/index.d.ts +1 -0
  46. package/dist/mixins/mixin-collection/index.js +1 -0
  47. package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
  48. package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
  49. package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
  50. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
  51. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
  52. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
  53. package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
  54. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
  55. package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
  56. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
  57. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
  58. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
  59. package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
  60. package/dist/mixins/mixin-field/mixin-field.js +423 -0
  61. package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
  62. package/dist/utils/anchor-element/anchor-element.js +39 -0
  63. package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
  64. package/dist/utils/capture-pointer/capture-pointer.js +30 -0
  65. package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
  66. package/dist/utils/closest-instance/closest-instance.js +34 -0
  67. package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
  68. package/dist/utils/create-sheet/create-sheet.js +16 -0
  69. package/dist/utils/custom-element/custom-element.d.ts +26 -0
  70. package/dist/utils/custom-element/custom-element.js +114 -0
  71. package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
  72. package/dist/utils/draw-icon/draw-icon.js +39 -0
  73. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
  74. package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
  75. package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
  76. package/dist/utils/elapsed-time/elapsed-time.js +117 -0
  77. package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
  78. package/dist/utils/focusable-elements/focusable-elements.js +88 -0
  79. package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
  80. package/dist/utils/format-numbers/format-numbers.js +29 -0
  81. package/dist/utils/host-aria/host-aria.d.ts +38 -0
  82. package/dist/utils/host-aria/host-aria.js +41 -0
  83. package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
  84. package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
  85. package/dist/utils/line-path/line-path.d.ts +23 -0
  86. package/dist/utils/line-path/line-path.js +107 -0
  87. package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
  88. package/dist/utils/on-unless-false/on-unless-false.js +32 -0
  89. package/dist/utils/read-series/read-series.d.ts +78 -0
  90. package/dist/utils/read-series/read-series.js +187 -0
  91. package/dist/utils/relative-time/relative-time.d.ts +24 -0
  92. package/dist/utils/relative-time/relative-time.js +84 -0
  93. package/dist/utils/request-change/request-change.d.ts +48 -0
  94. package/dist/utils/request-change/request-change.js +7 -0
  95. package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
  96. package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
  97. package/dist/utils/restore-value/restore-value.d.ts +32 -0
  98. package/dist/utils/restore-value/restore-value.js +84 -0
  99. package/dist/utils/scale/scale.d.ts +23 -0
  100. package/dist/utils/scale/scale.js +32 -0
  101. package/dist/utils/scale/ticks.d.ts +27 -0
  102. package/dist/utils/scale/ticks.js +52 -0
  103. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
  104. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
  105. package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
  106. package/dist/utils/slot-filled/slot-filled.js +26 -0
  107. package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
  108. package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
  109. package/dist/utils/ticker/ticker.d.ts +8 -0
  110. package/dist/utils/ticker/ticker.js +55 -0
  111. package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
  112. package/dist/utils/tone-marks/tone-marks.js +36 -0
  113. package/dist/utils/value-as/value-as.d.ts +64 -0
  114. package/dist/utils/value-as/value-as.js +105 -0
  115. package/package.json +58 -0
@@ -0,0 +1,67 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /** What the controller needs to know: which element inside is the control. */
3
+ export interface AriaForwardOptions {
4
+ /** The focusable thing in the shadow root, or null before it renders. */
5
+ target: () => HTMLElement | null;
6
+ }
7
+ /**
8
+ * Carries a host's ARIA references down to the control inside it.
9
+ *
10
+ * **The gap this closes, measured.** `<helix-button aria-describedby="tip">` puts
11
+ * the description on the host, and the host is not what a reader is on: the
12
+ * accessibility tree exposes the `<button>` in the shadow root, and the host
13
+ * beside it is a generic with a description nobody hears. Read from Chrome's own
14
+ * tree:
15
+ *
16
+ * ```
17
+ * host role=generic name="" desc="Helix description"
18
+ * inner button role=button name="Helix" desc=""
19
+ * ```
20
+ *
21
+ * A plain `<button>` with the identical attribute reads `desc="Helix
22
+ * description"`, so the reference itself is fine and only its destination is
23
+ * wrong. This moves it.
24
+ *
25
+ * **An IDREF cannot make that trip and an element reference can.** The ids are
26
+ * written in the consumer's tree and the control is in a shadow root, so
27
+ * `aria-describedby` copied onto the inner element resolves to nothing.
28
+ * `ariaDescribedByElements` takes the elements themselves, and a reference is
29
+ * valid into the same tree *or a shadow-including ancestor* of it: from inside
30
+ * the shadow root looking out, the document is exactly that. Measured, the
31
+ * assignment reads back with the node intact, where the inward direction, from a
32
+ * light-DOM trigger to a node in a shadow root, reads back empty. This is the
33
+ * one direction that works, which is why `Tooltip` needs `for` and this does
34
+ * not.
35
+ *
36
+ * **The attributes stay on the host.** Nothing is removed and nothing is added:
37
+ * the consumer's markup is exactly what they wrote, which is the same reason
38
+ * `hostAria` exists. What changes is only that the control now also holds the
39
+ * reference, so a screen reader on the focused element hears it.
40
+ *
41
+ * Watched with a `MutationObserver`, because a reference can arrive at any time:
42
+ * a framework rendering a tooltip after this element upgrades, or a consumer
43
+ * pointing `aria-describedby` at a different id, changes an attribute and
44
+ * nothing schedules a Lit update.
45
+ */
46
+ export declare class AriaForwardController implements ReactiveController {
47
+ private readonly host;
48
+ private readonly target;
49
+ private observer;
50
+ /** Which state attributes this controller wrote, so only those are removed. */
51
+ private readonly forwarded;
52
+ constructor(host: ReactiveControllerHost & HTMLElement, options: AriaForwardOptions);
53
+ /**
54
+ * Resolves the host's references and hands them to the control.
55
+ *
56
+ * Resolved through the host's own root rather than `document`, so a button
57
+ * inside another component's shadow root finds the ids written beside it
58
+ * there. Ids that resolve to nothing are dropped rather than passed on: an
59
+ * `aria-describedby` naming a heading that has been removed is a stale
60
+ * reference, and an empty array clears the property rather than leaving the
61
+ * last good one in place.
62
+ */
63
+ forward(): void;
64
+ hostConnected(): void;
65
+ hostDisconnected(): void;
66
+ hostUpdated(): void;
67
+ }
@@ -0,0 +1,146 @@
1
+ /** The two relationships a consumer writes on a host and means for the control. */
2
+ const RELATIONSHIPS = [
3
+ ['aria-describedby', 'ariaDescribedByElements'],
4
+ ['aria-labelledby', 'ariaLabelledByElements'],
5
+ ];
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
+ * **The case that asked for it was a disclosure with a slotted trigger.**
16
+ * `Megamenu` and `SpeedDial` write `aria-expanded` onto whatever a consumer
17
+ * slots, and where that is a `<helix-button>` the attribute landed on a host the
18
+ * tree reports as `generic`: measured through Chrome's own tree, a button named
19
+ * "Mega" with no expanded state at all, on a component whose comment says the
20
+ * write exists so a screen reader is not "told the control was a plain button
21
+ * forever". A plain `<button slot="trigger">` never had the problem, because
22
+ * there the host *is* the focusable node.
23
+ *
24
+ * **Only what this controller put there is ever taken away, and getting that
25
+ * wrong broke three components.** The first version removed any of these from
26
+ * the control whenever the host lacked it, which reads as the obvious way to
27
+ * keep the two in step and quietly deletes what the component renders for
28
+ * itself: `Combobox` writes `aria-controls="listbox"` onto its own input and
29
+ * `Select` writes `aria-expanded` onto its own trigger, and both were wiped on
30
+ * every update by a controller carrying nothing. Their own specs caught it.
31
+ *
32
+ * So the record below holds what was forwarded last time, and a host that stops
33
+ * carrying one removes exactly that and nothing else. A control's own attribute
34
+ * is untouched either way, which is the only rule that can be right: this
35
+ * controller knows what the host said and cannot know what the template meant.
36
+ */
37
+ const STATES = ['aria-expanded', 'aria-haspopup', 'aria-controls'];
38
+ /**
39
+ * Carries a host's ARIA references down to the control inside it.
40
+ *
41
+ * **The gap this closes, measured.** `<helix-button aria-describedby="tip">` puts
42
+ * the description on the host, and the host is not what a reader is on: the
43
+ * accessibility tree exposes the `<button>` in the shadow root, and the host
44
+ * beside it is a generic with a description nobody hears. Read from Chrome's own
45
+ * tree:
46
+ *
47
+ * ```
48
+ * host role=generic name="" desc="Helix description"
49
+ * inner button role=button name="Helix" desc=""
50
+ * ```
51
+ *
52
+ * A plain `<button>` with the identical attribute reads `desc="Helix
53
+ * description"`, so the reference itself is fine and only its destination is
54
+ * wrong. This moves it.
55
+ *
56
+ * **An IDREF cannot make that trip and an element reference can.** The ids are
57
+ * written in the consumer's tree and the control is in a shadow root, so
58
+ * `aria-describedby` copied onto the inner element resolves to nothing.
59
+ * `ariaDescribedByElements` takes the elements themselves, and a reference is
60
+ * valid into the same tree *or a shadow-including ancestor* of it: from inside
61
+ * the shadow root looking out, the document is exactly that. Measured, the
62
+ * assignment reads back with the node intact, where the inward direction, from a
63
+ * light-DOM trigger to a node in a shadow root, reads back empty. This is the
64
+ * one direction that works, which is why `Tooltip` needs `for` and this does
65
+ * not.
66
+ *
67
+ * **The attributes stay on the host.** Nothing is removed and nothing is added:
68
+ * the consumer's markup is exactly what they wrote, which is the same reason
69
+ * `hostAria` exists. What changes is only that the control now also holds the
70
+ * reference, so a screen reader on the focused element hears it.
71
+ *
72
+ * Watched with a `MutationObserver`, because a reference can arrive at any time:
73
+ * a framework rendering a tooltip after this element upgrades, or a consumer
74
+ * pointing `aria-describedby` at a different id, changes an attribute and
75
+ * nothing schedules a Lit update.
76
+ */
77
+ export class AriaForwardController {
78
+ constructor(host, options) {
79
+ /** Which state attributes this controller wrote, so only those are removed. */
80
+ this.forwarded = new Set();
81
+ this.host = host;
82
+ this.target = options.target;
83
+ host.addController(this);
84
+ }
85
+ /**
86
+ * Resolves the host's references and hands them to the control.
87
+ *
88
+ * Resolved through the host's own root rather than `document`, so a button
89
+ * inside another component's shadow root finds the ids written beside it
90
+ * there. Ids that resolve to nothing are dropped rather than passed on: an
91
+ * `aria-describedby` naming a heading that has been removed is a stale
92
+ * reference, and an empty array clears the property rather than leaving the
93
+ * last good one in place.
94
+ */
95
+ forward() {
96
+ const target = this.target();
97
+ if (!target) {
98
+ return;
99
+ }
100
+ const root = this.host.getRootNode();
101
+ for (const [attribute, property] of RELATIONSHIPS) {
102
+ if (!(property in target)) {
103
+ // An engine without element reflection. Nothing to do, and nothing that
104
+ // silently half works: assigning to a property it does not implement is
105
+ // a no-op rather than an error, which is what the guard makes visible.
106
+ continue;
107
+ }
108
+ const ids = (this.host.getAttribute(attribute) ?? '').split(/\s+/).filter(Boolean);
109
+ const elements = ids
110
+ .map((id) => root.getElementById?.(id) ?? null)
111
+ .filter((element) => element !== null);
112
+ target[property] =
113
+ elements.length > 0 ? elements : null;
114
+ }
115
+ for (const attribute of STATES) {
116
+ const value = this.host.getAttribute(attribute);
117
+ if (value === null) {
118
+ // Only if this controller is what put it there. See the note above.
119
+ if (this.forwarded.has(attribute)) {
120
+ target.removeAttribute(attribute);
121
+ this.forwarded.delete(attribute);
122
+ }
123
+ continue;
124
+ }
125
+ target.setAttribute(attribute, value);
126
+ this.forwarded.add(attribute);
127
+ }
128
+ }
129
+ hostConnected() {
130
+ this.observer = new MutationObserver(() => this.forward());
131
+ this.observer.observe(this.host, {
132
+ attributeFilter: [...RELATIONSHIPS.map(([attribute]) => attribute), ...STATES],
133
+ });
134
+ }
135
+ hostDisconnected() {
136
+ this.observer?.disconnect();
137
+ this.observer = undefined;
138
+ }
139
+ /* After the render that created the control, because there is nothing to
140
+ forward to before it exists. Every update, not only the first: a component
141
+ that re-renders its control, which `Button` does whenever `href` changes it
142
+ between an anchor and a button, produces a new element with none of this. */
143
+ hostUpdated() {
144
+ this.forward();
145
+ }
146
+ }
@@ -0,0 +1,79 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /** What the controller needs of its host, which is a Lit element and a count. */
3
+ export interface ChartPointerHost extends ReactiveControllerHost, HTMLElement {
4
+ /**
5
+ * How many readings there are, so the pointer has something to land on.
6
+ */
7
+ readonly readings: number;
8
+ /**
9
+ * Where the chart drew itself, which is where the plot is looked for.
10
+ */
11
+ readonly renderRoot: DocumentFragment | HTMLElement;
12
+ }
13
+ /**
14
+ * Which reading a pointer is nearest, for a chart that draws no markers.
15
+ *
16
+ * **The anchor is a position and not an element, and that is the whole reason
17
+ * this exists.** A bar and a donut slice are elements a tooltip could be hung
18
+ * on; a line is one `<path>` for a whole series, because
19
+ * `preserveAspectRatio="none"` turns a circle into an ellipse and there is no
20
+ * vector-effect that answers for a shape, so `LineChart` deliberately draws no
21
+ * per-reading marker. Anchoring per datum works for two charts of four and
22
+ * cannot be extended to the third, so the resolution is arithmetic instead.
23
+ *
24
+ * **It maps along one axis only.** The nearest reading to a pointer is decided
25
+ * by x alone in a vertical chart, whatever y is doing, because a reader moving
26
+ * across a chart is asking about a column of it rather than about a point. A
27
+ * true nearest-point search would make a two-series chart answer differently
28
+ * depending on which line the pointer drifted toward, which is a tooltip that
29
+ * changes its mind while the reader holds still.
30
+ *
31
+ * **Coarse pointers get nothing.** A touch has no hover, so a tooltip driven by
32
+ * one appears where a finger already covers it and stays after the finger
33
+ * lifts. The table underneath is the readable version and is what a phone
34
+ * should be given, so this listens for `pointermove` and ignores anything whose
35
+ * `pointerType` is not fine.
36
+ */
37
+ export declare class ChartPointerController implements ReactiveController {
38
+ #private;
39
+ /** The reading under the pointer, or -1 when the pointer is elsewhere. */
40
+ at: number;
41
+ /**
42
+ * Which axis the readings are laid out along.
43
+ *
44
+ * **An axis and not a boolean, because two of the four charts do not use x.**
45
+ * This was `tracks = true`, and every reading was resolved from the pointer's
46
+ * x whatever the chart was doing. A horizontal bar chart runs its categories
47
+ * *down* the box, so moving the pointer from the first row to the last never
48
+ * changed the answer: measured on four rows, all four reported the third,
49
+ * which is the slot the middle of the box falls in. It looked like a tooltip
50
+ * that would not follow and was a tooltip reading the wrong axis.
51
+ *
52
+ * `none` is the ring. Its slices are real elements at every angle, so the
53
+ * reading under the pointer is the one the pointer is over, and arithmetic on
54
+ * either axis would answer with whichever slice shared a coordinate: at three
55
+ * o'clock and at nine o'clock, x alone gives the same slice for opposite
56
+ * sides. The slices call `set` and this only holds the answer.
57
+ */
58
+ axis: 'block' | 'inline' | 'none';
59
+ constructor(host: ChartPointerHost);
60
+ hostConnected(): void;
61
+ hostDisconnected(): void;
62
+ /**
63
+ * Tells the tooltip its anchor moved, which nothing else would.
64
+ *
65
+ * The mark the tooltip is hung on is one element the chart walks from reading
66
+ * to reading with an inline style, and Floating UI's `autoUpdate` watches
67
+ * scrolling, resizing and layout shift. Moving an absolutely positioned
68
+ * element by writing `inset` is none of the three, so the bubble is placed
69
+ * once on open and then stays where the first reading was while the mark
70
+ * travels out from under it.
71
+ *
72
+ * Here rather than in the chart because the reading is this controller's, and
73
+ * after the update rather than in `set` because the mark is still at the old
74
+ * position until the render that follows.
75
+ */
76
+ hostUpdated(): void;
77
+ /** Puts the pointer on a reading, for a chart that resolves it some other way. */
78
+ set(at: number): void;
79
+ }
@@ -0,0 +1,140 @@
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
+ export class ChartPointerController {
26
+ #host;
27
+ #moved;
28
+ #plot;
29
+ constructor(host) {
30
+ this.#moved = false;
31
+ /** The reading under the pointer, or -1 when the pointer is elsewhere. */
32
+ 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
+ * This was `tracks = true`, and every reading was resolved from the pointer's
38
+ * x whatever the chart was doing. A horizontal bar chart runs its categories
39
+ * *down* the box, so moving the pointer from the first row to the last never
40
+ * changed the answer: measured on four rows, all four reported the third,
41
+ * which is the slot the middle of the box falls in. It looked like a tooltip
42
+ * that would not follow and was a tooltip reading the wrong axis.
43
+ *
44
+ * `none` is the ring. Its slices are real elements at every angle, so the
45
+ * reading under the pointer is the one the pointer is over, and arithmetic on
46
+ * either axis would answer with whichever slice shared a coordinate: at three
47
+ * o'clock and at nine o'clock, x alone gives the same slice for opposite
48
+ * sides. The slices call `set` and this only holds the answer.
49
+ */
50
+ this.axis = 'inline';
51
+ this.#move = (event) => {
52
+ if (this.axis === 'none' || (event.pointerType !== 'mouse' && event.pointerType !== 'pen')) {
53
+ return;
54
+ }
55
+ const box = this.#box();
56
+ const count = this.#host.readings;
57
+ const along = this.axis === 'block' ? box?.height : box?.width;
58
+ if (!box || !along || count === 0) {
59
+ return;
60
+ }
61
+ /* The same slot arithmetic the charts draw with: reading `i` sits in the
62
+ middle of the `i`th of `count` equal slots, so the nearest is the slot
63
+ the pointer is in. Rounding a proportion of `count - 1` instead puts the
64
+ boundaries half a slot off and the first and last reading get half the
65
+ catchment of the others. */
66
+ const from = this.axis === 'block' ? event.clientY - box.top : event.clientX - box.left;
67
+ const slot = Math.floor((from / along) * count);
68
+ const next = Math.min(Math.max(slot, 0), count - 1);
69
+ if (next !== this.at) {
70
+ this.at = next;
71
+ this.#moved = true;
72
+ this.#host.requestUpdate();
73
+ }
74
+ };
75
+ this.#leave = () => {
76
+ if (this.at !== -1) {
77
+ this.at = -1;
78
+ this.#host.requestUpdate();
79
+ }
80
+ };
81
+ this.#host = host;
82
+ host.addController(this);
83
+ }
84
+ hostConnected() {
85
+ this.#host.addEventListener('pointermove', this.#move);
86
+ this.#host.addEventListener('pointerleave', this.#leave);
87
+ }
88
+ hostDisconnected() {
89
+ this.#host.removeEventListener('pointermove', this.#move);
90
+ this.#host.removeEventListener('pointerleave', this.#leave);
91
+ this.at = -1;
92
+ }
93
+ /**
94
+ * Tells the tooltip its anchor moved, which nothing else would.
95
+ *
96
+ * The mark the tooltip is hung on is one element the chart walks from reading
97
+ * to reading with an inline style, and Floating UI's `autoUpdate` watches
98
+ * scrolling, resizing and layout shift. Moving an absolutely positioned
99
+ * element by writing `inset` is none of the three, so the bubble is placed
100
+ * once on open and then stays where the first reading was while the mark
101
+ * travels out from under it.
102
+ *
103
+ * Here rather than in the chart because the reading is this controller's, and
104
+ * after the update rather than in `set` because the mark is still at the old
105
+ * position until the render that follows.
106
+ */
107
+ hostUpdated() {
108
+ if (!this.#moved) {
109
+ return;
110
+ }
111
+ this.#moved = false;
112
+ this.#host.renderRoot
113
+ .querySelector('[part="tooltip-anchor"]')
114
+ ?.reposition?.();
115
+ }
116
+ /**
117
+ * The box the readings are laid out in, which is the plot and not the host.
118
+ *
119
+ * The host also holds the row of labels, so measuring against it would map
120
+ * the pointer over a box taller than the drawing. That mattered the moment a
121
+ * chart started measuring down the box rather than across it, which is what
122
+ * `axis` is for: on a horizontal bar chart the labels are beside the plot
123
+ * rather than under it, and the host is wider than the drawing by exactly
124
+ * their column.
125
+ */
126
+ #box() {
127
+ this.#plot ??= this.#host.renderRoot.querySelector('[part="plot"]') ?? undefined;
128
+ return this.#plot?.getBoundingClientRect();
129
+ }
130
+ /** Puts the pointer on a reading, for a chart that resolves it some other way. */
131
+ set(at) {
132
+ if (at !== this.at) {
133
+ this.at = at;
134
+ this.#moved = true;
135
+ this.#host.requestUpdate();
136
+ }
137
+ }
138
+ #move;
139
+ #leave;
140
+ }
@@ -0,0 +1,83 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * Whether the browser implements invoker commands.
4
+ *
5
+ * Feature-detected on the prototype rather than on an instance, so it can be
6
+ * answered before anything has rendered, and guarded for a server, which has no
7
+ * `HTMLButtonElement` at all.
8
+ */
9
+ export declare const supportsInvokers: () => boolean;
10
+ /**
11
+ * The `command` event, for a browser that does not send one.
12
+ *
13
+ * Invoker commands are recent: Chrome 135, Firefox 137, Safari 18.4. Before
14
+ * this, a button in an older browser set `commandForElement` on nothing, the
15
+ * wiring silently did not happen, and the control did nothing at all when
16
+ * clicked. No error, no warning, and every `command` in the library dead at
17
+ * once, which is the worst way for a feature to be missing: the markup is
18
+ * right, the component is right, and the button is simply inert.
19
+ *
20
+ * So the button sends this instead where the platform will not. It carries the
21
+ * two members `CommandController` reads and dispatches at the target, which is
22
+ * where the platform dispatches too, so nothing downstream can tell the
23
+ * difference.
24
+ *
25
+ * **The built-in commands are not reproduced.** `show-modal`, `close` and
26
+ * `toggle-popover` are defined against native elements and applied by the
27
+ * browser itself, and writing our own would mean reimplementing part of the
28
+ * platform against elements we do not own. Nothing in this library uses them:
29
+ * a custom element only ever answers a `--` command, which is the platform's
30
+ * marker for "the target decides", and the target is ours.
31
+ */
32
+ export declare class HelixFallbackCommandEvent extends Event {
33
+ /** The command asked for, verbatim. */
34
+ readonly command: string;
35
+ /** The element that asked. */
36
+ readonly source: Element | null;
37
+ constructor(command: string, source: Element | null);
38
+ }
39
+ /** What the host answers to, by command name. */
40
+ export interface CommandOptions {
41
+ /** Each command the host implements, keyed by the name a trigger writes. */
42
+ commands: Record<string, (source: Element | null) => void>;
43
+ }
44
+ /**
45
+ * Lets a control elsewhere on the page operate this one, declaratively.
46
+ *
47
+ * ```html
48
+ * <helix-button commandfor="confirm" command="--show">Delete…</helix-button>
49
+ * <helix-dialog id="confirm">…</helix-dialog>
50
+ * ```
51
+ *
52
+ * **The platform's own invoker, not a convention of ours.** `commandfor` and
53
+ * `command` are attributes the browser understands: it resolves the id, wires
54
+ * the two together, and dispatches a `command` event at the target when the
55
+ * trigger is activated. That is a real event with a real source, so it works
56
+ * for a click, for Enter, for Space, and for anything else the platform decides
57
+ * counts as activation later.
58
+ *
59
+ * The alternative was a `data-` recipe of our own with a delegated listener on
60
+ * the document, and it is worse in every direction: a global listener a
61
+ * consumer cannot see, an id lookup we would write ourselves, activation
62
+ * semantics we would have to guess at, and a convention that is ours to
63
+ * document and theirs to learn.
64
+ *
65
+ * **Only custom commands reach a custom element.** The built-in ones —
66
+ * `show-modal`, `close`, `toggle-popover` — are defined against native elements
67
+ * and the browser applies them itself; pointed at a custom element they do
68
+ * nothing. So the names here start with `--`, which is the platform's marker
69
+ * for "the target decides what this means", and the target deciding is exactly
70
+ * what this is.
71
+ *
72
+ * Unrecognized commands are left alone rather than swallowed, so a host that
73
+ * answers `--show` does not quietly absorb a `--print` meant for something
74
+ * else.
75
+ */
76
+ export declare class CommandController implements ReactiveController {
77
+ private readonly host;
78
+ private readonly options;
79
+ constructor(host: ReactiveControllerHost & HTMLElement, options: CommandOptions);
80
+ private readonly handle;
81
+ hostConnected(): void;
82
+ hostDisconnected(): void;
83
+ }
@@ -0,0 +1,93 @@
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
+ 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
+ export class HelixFallbackCommandEvent extends Event {
32
+ 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
+ super('command', { cancelable: true });
37
+ this.command = command;
38
+ this.source = source;
39
+ }
40
+ }
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
+ * The alternative was a `data-` recipe of our own with a delegated listener on
57
+ * the document, and it is worse in every direction: a global listener a
58
+ * consumer cannot see, an id lookup we would write ourselves, activation
59
+ * semantics we would have to guess at, and a convention that is ours to
60
+ * document and theirs to learn.
61
+ *
62
+ * **Only custom commands reach a custom element.** The built-in ones —
63
+ * `show-modal`, `close`, `toggle-popover` — are defined against native elements
64
+ * and the browser applies them itself; pointed at a custom element they do
65
+ * nothing. So the names here start with `--`, which is the platform's marker
66
+ * for "the target decides what this means", and the target deciding is exactly
67
+ * what this is.
68
+ *
69
+ * Unrecognized commands are left alone rather than swallowed, so a host that
70
+ * answers `--show` does not quietly absorb a `--print` meant for something
71
+ * else.
72
+ */
73
+ export class CommandController {
74
+ constructor(host, options) {
75
+ this.handle = (event) => {
76
+ const command = event.command;
77
+ const run = this.options.commands[command];
78
+ if (!run) {
79
+ return;
80
+ }
81
+ run(event.source);
82
+ };
83
+ this.host = host;
84
+ this.options = options;
85
+ host.addController(this);
86
+ }
87
+ hostConnected() {
88
+ this.host.addEventListener('command', this.handle);
89
+ }
90
+ hostDisconnected() {
91
+ this.host.removeEventListener('command', this.handle);
92
+ }
93
+ }
@@ -0,0 +1,58 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /** What a copy needs to know, asked for rather than assumed. */
3
+ export interface CopyOptions {
4
+ /**
5
+ * How long the confirmation stands, in milliseconds.
6
+ *
7
+ * On a timer rather than on the pointer leaving, so it survives a hand that
8
+ * has already moved on: the press and the reading of it are two moments, and
9
+ * a mark that vanishes with the cursor is one a fast reader never sees.
10
+ */
11
+ after?: number;
12
+ }
13
+ /**
14
+ * Puts text on the clipboard and holds a confirmation for a moment.
15
+ *
16
+ * A controller rather than a component, because copying is a behavior and not a
17
+ * shape. Color's chip is a swatch that copies, and a copy button is a button
18
+ * that copies, and those two have nothing in common to look at: what they share
19
+ * is the write, the confirmation that follows it, the timer that ends it and the
20
+ * event that announces it. Written twice, the second one would get the failure
21
+ * case wrong.
22
+ *
23
+ * **There are two routes and both are tried**, because the modern one is gated
24
+ * in more places than it looks: an insecure origin, an unfocused document, and a
25
+ * Permissions Policy defaulting to `self`, which a sandboxed iframe is not. The
26
+ * component using this reported as not working, and a control that does nothing
27
+ * is indistinguishable from a broken one. See `write` above.
28
+ *
29
+ * **Where neither route is allowed, the failure is silence.** The confirmation
30
+ * does not appear, which is the honest report: a message about a clipboard is
31
+ * worse than a chip that did not flash, and there is nothing a reader could do
32
+ * about a permission policy anyway.
33
+ *
34
+ * **The event is announced after the write, not before it.** It is not
35
+ * cancelable for the same reason: by the time anything could be said, the text
36
+ * is already on the clipboard, and an event that cannot prevent what it
37
+ * describes should not pretend otherwise.
38
+ *
39
+ * The timer is cleared when the host leaves the document, or a component removed
40
+ * mid-confirmation asks a detached element to re-render.
41
+ */
42
+ export declare class CopyController implements ReactiveController {
43
+ private readonly host;
44
+ private readonly options;
45
+ /** Whether the last write succeeded and is still being confirmed. */
46
+ copied: boolean;
47
+ private timer;
48
+ constructor(host: ReactiveControllerHost & EventTarget, options?: CopyOptions);
49
+ /**
50
+ * Writes to the clipboard, and confirms it if the platform allowed it.
51
+ *
52
+ * Empty text is not copied. An empty clipboard is not a thing a reader ever
53
+ * asks for, and confirming one is worse than doing nothing.
54
+ */
55
+ copy(text: string): Promise<void>;
56
+ hostDisconnected(): void;
57
+ private reset;
58
+ }