@fusion.dev/helix-headless 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -7,140 +7,25 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { AriaForwardController, customElement, HelixFormElement, MixinControllerValue, MixinField, restoreValue, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A value chosen from a range by dragging.
12
- *
13
- * @description
14
- * A real `<input type="range">`, so the drag, the arrow keys, Home and End, the
15
- * page keys and the touch target are all the platform's. Every one of those is
16
- * a thing a hand-built slider gets wrong, and the last is the one nobody
17
- * notices: a native range has a hit area larger than its thumb, sized by the
18
- * platform for a finger.
19
- *
20
- * **The value is a string, like every other form control's.** A range submits
21
- * text and `MixinControllerValue` holds text, so the number is a reading of the
22
- * value rather than the value itself. `valueAsNumber` is there for anything
23
- * doing arithmetic on it.
24
- *
25
- * **Marks are drawn rather than left to `<datalist>`.** A datalist's ticks
26
- * cannot be styled at all, and Firefox does not render them for a range at all,
27
- * so the platform's version is a mark that looks like someone else's design
28
- * where it appears and no mark where it does not. Drawing them costs the
29
- * semantics nothing, because a mark is decoration: what a reader can actually
30
- * land on is `step`, which the input enforces whether or not anything is drawn.
31
- *
32
- * `markers` is a count rather than a flag tied to `step`. A flag reads well
33
- * until a slider from zero to a hundred stepping by one asks for a hundred and
34
- * one ticks, and then it is either noise or a silent refusal to draw. A number
35
- * is what the reader sees, so it cannot surprise anyone.
36
- *
37
- * **A range is two of them, not a hand-built one.** There is no native
38
- * two-thumb range, so two are stacked on one track and each thumb keeps its own
39
- * drag, its own arrow keys, its own Home and End and its own finger-sized hit
40
- * area. Rebuilding would throw away every argument this component is made of,
41
- * to gain a second handle.
42
- *
43
- * Crossing is stopped in the handler rather than by the bounds, and that is not
44
- * the obvious way round. Giving the lower input `max` of the upper reads well
45
- * and breaks the drawing: an input's thumb sits at a fraction of *its own*
46
- * range, so a lower thumb bounded at the upper one paints against a shorter
47
- * track and the two stop lining up. Both inputs span the whole range and the
48
- * values are clamped against each other on the way in.
49
- *
50
- * The properties below are written by this component and read by whoever draws
51
- * it. A consumer sets none of them: they carry a number this element computed
52
- * and CSS cannot, which is what lets a stylesheet on this tier draw the thing
53
- * without measuring anything itself.
54
- *
55
- * @since 0.1.0
56
- * @status experimental
57
- * @category forms
58
- * @aliases Range, Range input
59
- * @documentation https://helix.fusion.dev/components/slider
60
- *
61
- * @event {Event} change - Relayed once the reader has let go.
62
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired as the value changes, and cancelable.
63
- * @event {Event} input - Announced once the value has moved.
64
- *
65
- * @csspart control - The range input.
66
- * @csspart feedback - What is wrong with it.
67
- * @csspart field - The row: the control and its readout.
68
- * @csspart hint - Help under the track, while nothing is wrong.
69
- * @csspart label - The question, over the track.
70
- * @csspart marker - One mark.
71
- * @csspart marker-label - The value a mark stands at, when `marker-labels` is set.
72
- * @csspart markers - The row of marks along the track.
73
- * @csspart output - The current value, shown beside it.
74
- *
75
- * @cssproperty --slider-fraction - The value as a fraction of the range, 0 to 1.
76
- * @cssproperty --slider-from - Where the filled span starts, as a percentage of the track.
77
- * @cssproperty --slider-marker-at - Where one marker sits, as a percentage of the track.
78
- * @cssproperty --slider-to - Where the filled span ends, as a percentage of the track.
79
- */
80
10
  let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue(MixinField(HelixFormElement)) {
81
11
  constructor() {
82
12
  super(...arguments);
83
- /* A consumer's `aria-describedby` names ids in their own tree and the control
84
- is in this shadow root, so the reference has to be carried down as an
85
- element rather than copied as an id. See `AriaForwardController`: measured
86
- in Chrome's own tree, the host held the description and the control the
87
- reader is actually on was exposed without it. */
88
13
  this.aria = new AriaForwardController(this, {
89
14
  target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
90
15
  });
91
- /** Typed or dragged, so every edit is announced. See `announcesInput`. */
92
16
  this.announcesInput = true;
93
- /**
94
- * Whether each mark says which value it stands at.
95
- */
96
17
  this.markerLabels = false;
97
- /**
98
- * How many marks to draw along the track, evenly spaced, ends included.
99
- *
100
- * `0` or `1` draws none: one mark on a track says nothing about a range.
101
- * They are decoration, so they are `aria-hidden`; what a reader can land on
102
- * is `step`, which the input enforces regardless.
103
- *
104
- * **Each one is snapped to a step before it is placed.** Evenly spaced and
105
- * on a step are two different things, and asking for four marks across a
106
- * hundred in twenty-fives wants them at 0, 33.3, 66.7 and 100: two of those
107
- * are places the thumb cannot go. A mark the thumb can never sit on is the
108
- * one thing a mark must not be, so the spacing gives way rather than the
109
- * step, and marks may come out unevenly spaced when the two disagree. That
110
- * is the honest picture of the range being offered.
111
- */
112
18
  this.markers = 0;
113
- /**
114
- * The largest value the reader can choose.
115
- */
116
19
  this.max = 100;
117
- /**
118
- * The smallest value the reader can choose.
119
- */
120
20
  this.min = 0;
121
- /**
122
- * Whether there are two thumbs and the value is a pair.
123
- *
124
- * The pair is one string, `"10,50"`, which is what a form submits and what
125
- * `MixinControllerValue` already holds. `valueAsNumber` reads the lower of
126
- * the two, since a range has no single number.
127
- */
128
21
  this.range = false;
129
- /**
130
- * Whether the chosen value is shown beside the track.
131
- */
132
22
  this.showValue = false;
133
23
  this.handleInput = (event) => {
134
- // Stopped for the reason `Input.handleInput` gives: the inner control's own
135
- // `input` is composed and would reach a consumer beside this element's.
136
24
  event.stopPropagation();
137
25
  const control = event.target;
138
26
  if (this.range) {
139
27
  const [start, end] = this.bounds;
140
28
  const moved = Number(control.value);
141
- /* Clamped against the other thumb rather than bounded by it, for the
142
- reason the class says: bounding shortens the input's own range and the
143
- two thumbs stop sharing a track. */
144
29
  const pair = control.dataset.thumb === 'start'
145
30
  ? [Math.min(moved, end), end]
146
31
  : [start, Math.max(moved, start)];
@@ -149,58 +34,22 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
149
34
  return;
150
35
  }
151
36
  const next = control.value;
152
- /* Through the mixin, like the other native-backed controls. The thumb has
153
- already moved either way, so wherever the answer does not follow it has
154
- to be put back: a refusal and a consumer who owns the value are the same
155
- situation seen from here. */
156
37
  this.requestValue(next);
157
38
  restoreValue(control, this.value);
158
39
  };
159
- /** Relayed, because `change` does not cross a shadow boundary on its own. */
160
40
  this.handleChange = (event) => {
161
41
  event.stopPropagation();
162
42
  this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
163
43
  };
164
- /** How far one press of an arrow key moves it. */
165
44
  this.step = 1;
166
45
  }
167
- /**
168
- * How many characters the readout has to be able to hold, widest case.
169
- *
170
- * **A number that grows a digit makes the track jump.** The output sizes to
171
- * its text, so a range crossing 99 to 100 widens by a character and the row
172
- * gives that width back out of the track: the handle moves while the value
173
- * has not, which reads as the control drifting under the pointer. `3ch` was
174
- * the floor and covers a plain value up to three digits, and a range says
175
- * "35 to 100", which is ten.
176
- *
177
- * Widest rather than current, so the box never changes size at all. Measured
178
- * off the bounds because that is what the readout can ever say: the longer of
179
- * the two ends, twice, plus the four of " to " when both are shown. Negative
180
- * bounds are covered by measuring the rendered string rather than the digits,
181
- * since the sign is a character like any other.
182
- *
183
- * `tabular-nums` is on the part already and is the other half of this: equal
184
- * digit widths keep a same-length number from shifting inside the box the
185
- * same way a different-length one shifted the box.
186
- */
187
46
  get reserve() {
188
47
  const widest = Math.max(String(this.min).length, String(this.max).length);
189
48
  return this.range ? widest * 2 + ' to '.length : widest;
190
49
  }
191
- /**
192
- * How many decimals the step is written to, so a label is not float noise.
193
- *
194
- * `min + index * step` on a fractional step prints `0.30000000000000004`,
195
- * which is arithmetic being honest and a label being wrong. Rounding back to
196
- * the step's own precision is what the reader asked for when they wrote it.
197
- */
198
50
  get precision() {
199
51
  return String(this.step).split('.')[1]?.length ?? 0;
200
52
  }
201
- /**
202
- * The values the marks stand at, evenly spaced, each snapped to a step.
203
- */
204
53
  get markerValues() {
205
54
  const span = this.max - this.min;
206
55
  if (this.markers < 2 || span <= 0) {
@@ -210,42 +59,19 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
210
59
  const snapped = Array.from({ length: this.markers }, (_, at) => {
211
60
  const raw = this.min + (at / (this.markers - 1)) * span;
212
61
  const snappedAt = Math.min(this.max, Math.max(this.min, this.min + Math.round((raw - this.min) / step) * step));
213
- // Back to the step's own precision, or a fractional step prints
214
- // `0.30000000000000004` on a marker label.
215
62
  return Number(snappedAt.toFixed(this.precision));
216
63
  });
217
64
  return [...new Set(snapped)];
218
65
  }
219
- /**
220
- * The chosen value as a number, for anything doing arithmetic on it.
221
- *
222
- * @reference https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasnumber HTMLInputElement.valueAsNumber
223
- */
224
66
  get valueAsNumber() {
225
67
  const parsed = Number.parseFloat(this.value);
226
68
  return Number.isNaN(parsed) ? this.min : parsed;
227
69
  }
228
- /** Focus is delegated to the control, so the host is never the target. */
229
70
  static { this.shadowRootOptions = {
230
71
  ...HelixFormElement.shadowRootOptions,
231
72
  delegatesFocus: true,
232
73
  }; }
233
- /**
234
- * The pair, as numbers, in order.
235
- *
236
- * Falls back to the whole range rather than to zero, so a range slider given
237
- * no value opens fully wide instead of pinned shut at its own minimum, which
238
- * is what a filter with nothing filtered out looks like.
239
- */
240
74
  get bounds() {
241
- /* A lone number is not a range, so it is not read as one.
242
- *
243
- * An untouched range slider does not hold an empty string: the native input
244
- * reports its own midpoint and the mixin takes it, so `value` came back as
245
- * "50" on a slider from 10 to 90 and reading that as the lower thumb put a
246
- * filter half shut before anyone touched it. Only a pair is a pair, and
247
- * anything else opens fully wide, which is what "nothing filtered out"
248
- * looks like. */
249
75
  const parts = this.value.split(',');
250
76
  const from = Number.parseFloat(parts[0] ?? '');
251
77
  const to = Number.parseFloat(parts[1] ?? '');
@@ -254,7 +80,6 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
254
80
  }
255
81
  return [Math.min(from, to), Math.max(from, to)];
256
82
  }
257
- /** Where one value sits along the track, as a fraction of it. */
258
83
  along(at) {
259
84
  const span = this.max - this.min;
260
85
  if (span === 0) {
@@ -262,30 +87,10 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
262
87
  }
263
88
  return Math.min(1, Math.max(0, (at - this.min) / span));
264
89
  }
265
- /** Reports the value. */
266
90
  sync() {
267
91
  this.internals.setFormValue(this.value);
268
92
  this.refreshFeedback();
269
93
  }
270
- /**
271
- * How far along the track the thumb sits, as a fraction.
272
- *
273
- * Arithmetic rather than a measurement, because the browser already knows
274
- * where it drew the handle and measuring it would be a guess at that.
275
- *
276
- * **Sanitized here, never read back off the control.** Asking the input for
277
- * its value reads well, a range clamping and snapping whatever it is given,
278
- * and the timing defeats it: `fraction` is evaluated inside `render()` and the
279
- * `style` binding carrying it is committed *before* the `.value` binding below
280
- * it, so it reads the value this very render is about to replace. The fill
281
- * ends up a render behind the thumb, and on the first pass it reads the seeded
282
- * midpoint and stays there: stable and off by half a track, which reads as a
283
- * drawing bug rather than a timing one.
284
- *
285
- * So the same two operations a range performs are done here: snap to the step
286
- * from `min`, then clamp to the bounds. Nothing is read from the tree, so
287
- * there is no order for this to be on the wrong side of.
288
- */
289
94
  get fraction() {
290
95
  const span = this.max - this.min;
291
96
  if (span === 0) {
@@ -293,15 +98,6 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
293
98
  }
294
99
  return this.along(this.snapped(this.valueAsNumber));
295
100
  }
296
- /**
297
- * A value snapped to the step, counted from `min` the way a range counts it.
298
- *
299
- * `min` rather than zero is the whole of it: a slider from `0.2` with a step
300
- * of `0.05` can land on `0.2`, `0.25` and so on, and snapping against zero
301
- * would offer `0.2` and `0.25` too but round the ones between to the wrong
302
- * side. `precision` is what keeps `0.30000000000000004` off the track, the
303
- * same guard the marker labels already use.
304
- */
305
101
  snapped(at) {
306
102
  if (!(this.step > 0)) {
307
103
  return at;
@@ -311,16 +107,7 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
311
107
  }
312
108
  willUpdate(changed) {
313
109
  super.willUpdate(changed);
314
- /* A range with no value starts in the middle, which is the platform's own
315
- rule and is what the control will show whether or not this agrees. Seeded
316
- here so the value the form would submit matches the thumb from the first
317
- render rather than from the first drag. */
318
110
  if (!this.hasUpdated && this.value === '') {
319
- /* Onto `defaultValue`, never onto `value`. `defaultValue` is where the
320
- markup's intent lives and where a reset returns to, and "no value given"
321
- is exactly a fact about the markup. Writing `value` would record the
322
- midpoint as something the reader chose, so a form reset would return to
323
- it rather than clearing it. */
324
111
  this.defaultValue = String(this.min + (this.max - this.min) / 2);
325
112
  }
326
113
  }
@@ -329,33 +116,7 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
329
116
  this.sync();
330
117
  }
331
118
  render() {
332
- /* The range's bounds are written before its value below, and that order is
333
- the whole of this element's correctness on a wide scale. Lit commits
334
- bindings in template order, and a range clamps whatever is assigned to the
335
- bounds it holds at that moment: with the value first, a slider from 0 to
336
- 360 given 225 took the default max of 100, clamped to 100, and drew its
337
- handle at 100/360.
338
-
339
- The symptom is the fill and the handle disagreeing, because they are
340
- computed from different things: the fill is arithmetic on this element's
341
- property, and the handle is the platform's reading of the input's own
342
- clamped value. It appears on any slider with a max above 100, and the
343
- fill being right is what makes it read as a drawing bug rather than a
344
- value one. */
345
119
  const [start, end] = this.bounds;
346
- /* One input per thumb, each spanning the whole range so both read against
347
- the same track. The lower one paints the fill and the upper one's track
348
- is transparent, because two stacked tracks would paint the ramp twice. */
349
- /* **The lower thumb answers to `control`, and it has to answer to
350
- something.** `MixinField.renderLabel` writes `<label for="control">` for
351
- every field in the library, so a range branch that does not render that id
352
- ships a label pointing at nothing: pressing the words focuses no thumb and
353
- the `<output for>` beside it names a missing element too. Valid markup
354
- that reaches nothing and says nothing.
355
-
356
- The lower one, because a label forwards a press to the first control it
357
- names and the reader expects the start of the range. The upper takes
358
- `control-end`, which is what the readout lists alongside it. */
359
120
  const thumb = (which, at) => html `<input
360
121
  part="control"
361
122
  id=${which === 'start' ? 'control' : 'control-end'}
@@ -372,25 +133,6 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
372
133
  @input=${this.handleInput}
373
134
  @change=${this.handleChange}
374
135
  />`;
375
- /* `data-markers` mirrors "there are marks" for the stylesheet.
376
- *
377
- * `markers` does not reflect, so a marker layout hung off `:host([markers])`
378
- * draws nothing for `slider.markers = 5`: only the attribute written in the
379
- * markup works, and the property that is the documented API silently does
380
- * not.
381
- *
382
- * Reflecting the count instead would put `markers="0"` on every slider, an
383
- * attribute the server's markup does not have and one that selector would
384
- * match, drawing the layout for no marks. So the boolean fact is mirrored
385
- * to a `data-` name, which is the rule this library already follows for
386
- * state: the stylesheet wants to know whether there are marks, not how
387
- * many. How many is what `markerValues` answers, and `< 2` is where it
388
- * already draws the line.
389
- *
390
- * A `/*` and written here, because it describes this binding and not a
391
- * member. Nothing catches an unclosed block, only one sitting on another,
392
- * so a `/**` that swallows the member below it ships as that member's
393
- * description. */
394
136
  return html `${this.renderLabel()}
395
137
  <div
396
138
  part="field"
@@ -426,15 +168,7 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
426
168
  >`)}
427
169
  </div>`
428
170
  : nothing}
429
- ${
430
- /* Hidden from the accessibility tree, like the markers above it.
431
- *
432
- * A bare `<output>` maps to `role="status"` with an implicit polite
433
- * live region, so every intermediate value during a drag was announced
434
- * on top of the range's own value announcement: a reader dragging from
435
- * 10 to 60 heard both twice, fifty times. It repeats what the control
436
- * already reports, which is the definition of decoration. */
437
- this.showValue
171
+ ${this.showValue
438
172
  ? html `<output
439
173
  part="output"
440
174
  for=${this.range ? 'control control-end' : 'control'}
@@ -7,99 +7,18 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, extent, HelixElement, linear, linePath, SourceController, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * The drawing's own coordinates, which the `viewBox` maps onto whatever box CSS
12
- * gives it. Round numbers so a `d` attribute is readable in the inspector.
13
- */
14
10
  const BOX = { height: 32, width: 100 };
15
- /** Splits on whitespace or commas, and keeps only what is a number. */
16
11
  const readValues = (input) => input
17
12
  .split(/[\s,]+/)
18
13
  .filter((one) => one !== '')
19
14
  .map(Number)
20
15
  .filter((one) => Number.isFinite(one));
21
- /**
22
- * @summary A line with no axes, for the shape of a series beside the number it belongs to.
23
- *
24
- * @description
25
- * The one chart here that takes its numbers from an attribute, and the reversal
26
- * is deliberate. Every other chart reads a table because a reader might want to
27
- * interrogate it; nobody interrogates the squiggle next to a figure. A
28
- * fourteen-row table behind a bare trend line is fourteen dates read aloud in
29
- * place of the one sentence that carries the meaning, so `values` is the list
30
- * and `label` is the sentence.
31
- *
32
- * That is the library's own rule rather than a second data channel: a content
33
- * property is its slot's fallback, which is why `Tooltip` takes `content` and
34
- * `Avatar` takes `name`. `for` stays for the sparkline that genuinely has a
35
- * table behind it, and it wins where both are given.
36
- *
37
- * **No label means decorative, and that is a choice rather than an oversight.**
38
- * A sparkline beside a stat that already says "Sessions, 1,102" has nothing of
39
- * its own to announce, and an unnamed `role="img"` announces "graphic" and
40
- * stops. So an unlabeled sparkline is `aria-hidden`, and a labeled one is an
41
- * image named by its sentence.
42
- *
43
- * It goes *beside* a `Stat` rather than inside one: a change beside a figure is
44
- * a second fact, which is what `Stat`'s own spec says. `Trend` composes the same
45
- * way.
46
- *
47
- * The properties below are written by this component and read by whoever draws
48
- * it. A consumer sets none of them: they carry a number this element computed
49
- * and CSS cannot, which is what lets a stylesheet on this tier draw the thing
50
- * without measuring anything itself.
51
- *
52
- * @since 0.1.0
53
- * @status experimental
54
- * @category charts
55
- * @aliases Micro Chart, Trend Line, Inline Chart
56
- * @documentation https://helix.fusion.dev/components/sparkline
57
- *
58
- * @csspart area - The region between the line and the bottom of its range.
59
- * @csspart bar - One column, under `plot="bar"`.
60
- * @csspart line - The path itself.
61
- * @csspart svg - The drawing.
62
- * @csspart terminus - The mark on the last value.
63
- * @csspart wash-end - The far stop of the area's gradient.
64
- * @csspart wash-start - The stop of the area's gradient that meets the line.
65
- *
66
- * @cssproperty --chart-terminus-x - Where the last point sits horizontally, as a percentage.
67
- * @cssproperty --chart-terminus-y - Where the last point sits vertically, as a percentage.
68
- */
69
16
  let HelixHeadlessSparkline = class HelixHeadlessSparkline extends HelixElement {
70
17
  constructor() {
71
18
  super(...arguments);
72
- /**
73
- * The id of a table to read instead, when there is one behind the line.
74
- *
75
- * The first column of numbers is the series drawn. This wins over `values`,
76
- * because a table is the readable representation and a list of numbers in an
77
- * attribute is not.
78
- *
79
- * **In React this arrives as a property and not as an attribute**, the way
80
- * `Tooltip.for` does, so it does not reflect and nothing styles on it.
81
- */
82
19
  this.for = '';
83
- /**
84
- * What the line says, in a sentence.
85
- *
86
- * "Sessions rose from 1,240 to 1,318 and fell to 1,102 over fourteen days" is
87
- * the accessible representation of a sparkline, and a list of the fourteen
88
- * numbers is not. Absent, the drawing is hidden from a screen reader rather
89
- * than announced without a name.
90
- */
91
20
  this.label = '';
92
- /**
93
- * The numbers, separated by whitespace or by commas.
94
- *
95
- * Both separators, so `values="1240 1318"` and an array assigned to the
96
- * property both work: assigning `[1240, 1318]` stringifies to `1240,1318`,
97
- * and a string is what survives React's hydration where an array property
98
- * does not.
99
- */
100
21
  this.values = '';
101
- /* One controller rather than the resolve-and-watch each chart had written
102
- out for itself. */
103
22
  this.source = new SourceController(this);
104
23
  }
105
24
  willUpdate(changed) {
@@ -107,7 +26,6 @@ let HelixHeadlessSparkline = class HelixHeadlessSparkline extends HelixElement {
107
26
  this.source.watch();
108
27
  }
109
28
  }
110
- /** The series to draw: the table if there is one, the attribute otherwise. */
111
29
  #points() {
112
30
  const series = this.source.series;
113
31
  return series.length > 0
@@ -116,75 +34,26 @@ let HelixHeadlessSparkline = class HelixHeadlessSparkline extends HelixElement {
116
34
  }
117
35
  render() {
118
36
  const points = this.#points();
119
- /* Two points minimum, because one is not a trend and zero is not a line.
120
- An `<svg>` given a `d` of `M NaN,NaN` draws nothing and reports nothing,
121
- so the empty case is stated here rather than left to look like a bug. */
122
37
  const x = linear([0, Math.max(points.length - 1, 1)], [0, BOX.width]);
123
- /* **A bar measures from zero and a line measures from its own range**, and
124
- that is a correctness difference rather than a preference. A bar's
125
- *length* is the value, so a baseline anywhere but zero makes the lengths
126
- proportional to nothing. Drawn over the data's extent, the smallest
127
- reading lands exactly on the floor and its column has no height at all:
128
- seven readings between 1,102 and 1,408 come out as six columns and a gap,
129
- which looks like missing data and is really a scale saying the minimum is
130
- nothing.
131
-
132
- The zero is included at both ends, so a series that goes negative gets a
133
- baseline inside the box and its columns hang below it. */
134
38
  const [low, high] = extent(points.length > 0 ? points : [0]);
135
39
  const y = this.plot === 'bar'
136
40
  ? linear([Math.min(0, low), Math.max(0, high)], [BOX.height, 0])
137
41
  : linear([low, high], [BOX.height, 0]);
138
42
  const coordinates = points.map((value, index) => [x(index), y(value)]);
139
43
  const path = this.plot === 'bar' ? '' : linePath(coordinates, this.curve ?? 'smooth');
140
- /* The same line closed down to the floor of its own range, which is where
141
- the wash goes. Its own path rather than a fill on the line, because a
142
- closed path strokes its closing edges too: filling the line itself would
143
- draw a rule along the bottom of the box and up both sides.
144
-
145
- The floor is the smallest reading and not a true zero, because the scale
146
- runs over the data's own extent. */
147
- /* Asked negatively, because absent *is* an area here and the class doc
148
- above says why. This is the one property in the family where the resting
149
- arrangement is not the plainest thing, so the test has to answer true for
150
- the value nobody set, and `=== 'area'` would answer false. */
151
44
  const area = this.plot !== 'bar' && this.plot !== 'line' && path !== ''
152
45
  ? `${path}L${BOX.width},${BOX.height}L0,${BOX.height}Z`
153
46
  : '';
154
- /* One column per reading, inset so neighbors do not touch.
155
- *
156
- * A rect rather than a thick stroked line, because a bar has two ends and a
157
- * stroke has a cap: `stroke-linecap` would round or square both ends of
158
- * every column together, and the floor end should always be flat.
159
- *
160
- * The width is a share of the slot rather than a fixed number, so a series
161
- * of seven and a series of forty both fill the box. Fourteen percent of the
162
- * gap is the smallest separation that still reads as separate columns at
163
- * the sizes a sparkline is drawn at, and it comes off the slot rather than
164
- * off the bar so a two-reading series does not get two enormous slabs. */
165
47
  const slot = BOX.width / Math.max(points.length, 1);
166
48
  const floor = y(0);
167
49
  const bars = this.plot === 'bar'
168
50
  ? points.map((value, index) => ({
169
- /* From the baseline to the reading, whichever of the two is
170
- higher up the box. A reading of exactly zero is a column of no
171
- height, which paints nothing, and that is the honest drawing of
172
- nothing rather than a hairline implying something. */
173
51
  height: Math.abs(floor - y(value)),
174
52
  width: slot * 0.72,
175
53
  x: index * slot + slot * 0.14,
176
54
  y: Math.min(floor, y(value)),
177
55
  }))
178
56
  : [];
179
- /* Where the series ended, as percentages of the box.
180
- *
181
- * Read twice, by the mark and by the drawing, which is why it is a local
182
- * rather than two literals. The mark sits on it; the drawing has the same
183
- * disc taken out of it, so a `ring` is a hole rather than a lid. A custom
184
- * property would be the tidier channel and there is nowhere to put one:
185
- * they inherit down and never sideways, the two elements are siblings, and
186
- * the host cannot be styled from a template without a wrapper element that
187
- * carries nothing else. */
188
57
  const end = points.length < 2
189
58
  ? undefined
190
59
  : `--chart-terminus-x: 100%; --chart-terminus-y: ${(y(points[points.length - 1]) / BOX.height) * 100}%`;
@@ -197,33 +66,13 @@ let HelixHeadlessSparkline = class HelixHeadlessSparkline extends HelixElement {
197
66
  style=${end ?? nothing}
198
67
  viewBox="0 0 ${BOX.width} ${BOX.height}"
199
68
  >
200
- ${
201
- /* The ramp the area is poured with, drawn here because a gradient is
202
- an element and elements are this tier's business, and colored
203
- above because a color is not. Measured rather than assumed: a
204
- fragment id inside a shadow root resolves within that root, so two
205
- sparklines on a page carrying the same id stay independent, and CSS
206
- `fill: url(#...)` reaches it as readily as the attribute does. */
207
- svg `<defs>
69
+ ${svg `<defs>
208
70
  <linearGradient id="sparkline-wash" x1="0" x2="0" y1="0" y2="1">
209
71
  <stop offset="0" part="wash-start" />
210
72
  <stop offset="1" part="wash-end" />
211
73
  </linearGradient>
212
74
  </defs>`}
213
- ${
214
- /* `svg` and not `html`, because a nested template is parsed on its
215
- own and this one has no `<svg>` around it to be parsed inside.
216
- `<path>` read as standalone HTML becomes an unknown element in the
217
- XHTML namespace: it takes the part, takes the `d`, inherits the
218
- stroke, and draws nothing at all, because only an element in the
219
- SVG namespace has geometry. Measured by `getBBox` throwing, which
220
- is the shortest way to ask an element whether it is really SVG.
221
-
222
- Every spec written against this passed while it drew nothing. They
223
- asserted the `d` attribute, the part, the ARIA and the computed
224
- `vector-effect`, and all of those are true of an HTML element
225
- wearing the same name. */
226
- path === '' ? nothing : svg `<path d=${path} part="line" />`}
75
+ ${path === '' ? nothing : svg `<path d=${path} part="line" />`}
227
76
  ${area === '' ? nothing : svg `<path d=${area} part="area" />`}
228
77
  ${bars.map((bar) => svg `<rect
229
78
  height=${bar.height}
@@ -233,17 +82,7 @@ let HelixHeadlessSparkline = class HelixHeadlessSparkline extends HelixElement {
233
82
  y=${bar.y}
234
83
  />`)}
235
84
  </svg>
236
- ${
237
- /* The mark on the last value, as an element beside the drawing rather
238
- than a `<circle>` inside it. `preserveAspectRatio="none"` is what lets
239
- the line fill any box, and it distorts everything in that coordinate
240
- space with it: a circle in there is an ellipse whose squash depends on
241
- how wide the consumer made it. Percentages of the host are the same
242
- two numbers in a space nothing stretches. */
243
- /* A line ends somewhere and a row of columns does not, so a bar plot
244
- draws no terminus: the last column already is the last reading, and a
245
- dot on top of it would be a second mark saying the same thing. */
246
- points.length < 2 || this.plot === 'bar'
85
+ ${points.length < 2 || this.plot === 'bar'
247
86
  ? nothing
248
87
  : html `<span aria-hidden="true" part="terminus" style=${end ?? nothing}></span>`}`;
249
88
  }