@fusion.dev/helix-headless 0.2.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 +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- 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
|
}
|