@fusion.dev/helix-headless 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +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,12 +7,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, HelixShortcutEvent } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* Whether a node is something a reader types into.
|
|
12
|
-
*
|
|
13
|
-
* The input types are the ones that take text. A checkbox and a range are
|
|
14
|
-
* `<input>` and are not typing, so a chord still reaches them.
|
|
15
|
-
*/
|
|
16
10
|
const typing = (node) => {
|
|
17
11
|
if (node instanceof HTMLTextAreaElement) {
|
|
18
12
|
return true;
|
|
@@ -22,7 +16,6 @@ const typing = (node) => {
|
|
|
22
16
|
}
|
|
23
17
|
return node instanceof HTMLInputElement && !UNTYPED.has(node.type);
|
|
24
18
|
};
|
|
25
|
-
/** `<input>` types that hold no text, so a bare chord is not competing with one. */
|
|
26
19
|
const UNTYPED = new Set([
|
|
27
20
|
'button',
|
|
28
21
|
'checkbox',
|
|
@@ -35,14 +28,6 @@ const UNTYPED = new Set([
|
|
|
35
28
|
'reset',
|
|
36
29
|
'submit',
|
|
37
30
|
]);
|
|
38
|
-
/**
|
|
39
|
-
* How each name in a chord is drawn, per platform.
|
|
40
|
-
*
|
|
41
|
-
* Apple keyboards label their modifiers with glyphs and print no separator
|
|
42
|
-
* between them, so `⌘K` is the whole of it. Everywhere else the words are the
|
|
43
|
-
* label and the plus is part of the convention rather than prose, which is why
|
|
44
|
-
* it is the one separator this component supplies.
|
|
45
|
-
*/
|
|
46
31
|
const APPLE = {
|
|
47
32
|
alt: '⌥',
|
|
48
33
|
backspace: '⌫',
|
|
@@ -81,109 +66,22 @@ const ELSEWHERE = {
|
|
|
81
66
|
tab: 'Tab',
|
|
82
67
|
up: '↑',
|
|
83
68
|
};
|
|
84
|
-
/** Whether the keyboard in front of the reader labels its modifiers with glyphs. */
|
|
85
69
|
const onApple = () => typeof navigator !== 'undefined' && /mac|iphone|ipad|ipod/i.test(navigator.userAgent);
|
|
86
|
-
/**
|
|
87
|
-
* @summary A keyboard key or chord, set inline with text.
|
|
88
|
-
*
|
|
89
|
-
* @description
|
|
90
|
-
* Renders a real `<kbd>`, so the passage still means "press this" to anything
|
|
91
|
-
* reading the markup rather than only to a reader looking at it.
|
|
92
|
-
*
|
|
93
|
-
* **One key per element, with `keys` as the stated exception.** A *sequence* is
|
|
94
|
-
* several of these with the separator written between them as ordinary text,
|
|
95
|
-
* because that separator is prose: it is "then" in a sequence and "plus" in a
|
|
96
|
-
* simultaneous press, and a component that inserted one would have to guess
|
|
97
|
-
* which. `keys` cannot be guessing, since a chord written `mod+k` has said
|
|
98
|
-
* which it is.
|
|
99
|
-
*
|
|
100
|
-
* **`keys` exists because the glyphs were being typed by hand and were wrong.**
|
|
101
|
-
* A literal `⌘K` in a template tells a reader on Windows to press a key their
|
|
102
|
-
* keyboard does not have, and nothing catches it, a string being a string.
|
|
103
|
-
* Given `mod` this renders `⌘K` on Apple and `Ctrl+K` everywhere else.
|
|
104
|
-
*
|
|
105
|
-
* **`within` is what binds it, and it is absent by default.** Without it this
|
|
106
|
-
* draws a key and listens for nothing, which is what every existing use of this
|
|
107
|
-
* component wants. The two values are the two honest lifetimes:
|
|
108
|
-
*
|
|
109
|
-
* - `control` listens within the element's own parent, which is the case this
|
|
110
|
-
* is mostly for: a hint sitting inside the control it belongs to, where the
|
|
111
|
-
* shortcut should work while that control has focus and stop existing when
|
|
112
|
-
* the control does.
|
|
113
|
-
* - `document` listens on the document, for a chord that works from anywhere.
|
|
114
|
-
*
|
|
115
|
-
* **A hidden `kbd` is a dead shortcut, and that is the point rather than a
|
|
116
|
-
* trap.** Binding here ties the key to its own advertisement, so a hint that is
|
|
117
|
-
* not rendered is a shortcut that is not offered. A chord that must outlive its
|
|
118
|
-
* label is a chord whose owner is not a label: give it to whatever owns the
|
|
119
|
-
* action and let this element only describe it.
|
|
120
|
-
*
|
|
121
|
-
* Nothing binds until this upgrades, so a shortcut is not available in the
|
|
122
|
-
* moments before hydration.
|
|
123
|
-
*
|
|
124
|
-
* @since 0.1.0
|
|
125
|
-
* @status stable
|
|
126
|
-
* @category typography
|
|
127
|
-
* @aliases Key, Shortcut, Hotkey
|
|
128
|
-
* @documentation https://helix.fusion.dev/components/kbd
|
|
129
|
-
*
|
|
130
|
-
* @slot - The key's label, where `keys` is not set.
|
|
131
|
-
*
|
|
132
|
-
* @event {HelixShortcutEvent} helix-shortcut - Fired when the chord is pressed, and cancelable.
|
|
133
|
-
*
|
|
134
|
-
* @csspart kbd - The key.
|
|
135
|
-
*/
|
|
136
70
|
let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
|
|
137
71
|
constructor() {
|
|
138
72
|
super(...arguments);
|
|
139
|
-
/**
|
|
140
|
-
* The chord, written plainly: `mod+k`, `shift+?`, `/`.
|
|
141
|
-
*
|
|
142
|
-
* `mod` is the platform's own command modifier, which is the reason to write
|
|
143
|
-
* a chord here rather than a glyph: it is the one token whose right answer
|
|
144
|
-
* differs per reader.
|
|
145
|
-
*/
|
|
146
73
|
this.keys = '';
|
|
147
74
|
this.handleKeydown = (event) => {
|
|
148
75
|
const press = event;
|
|
149
|
-
/* Mid-composition is not a chord. An IME sends keystrokes that belong to
|
|
150
|
-
the text being composed, and taking one is how a shortcut eats a
|
|
151
|
-
character the reader was trying to type. */
|
|
152
76
|
if (press.isComposing) {
|
|
153
77
|
return;
|
|
154
78
|
}
|
|
155
|
-
/* Neither is a bare character typed into a field, which is the same fault
|
|
156
|
-
the IME guard above already refuses: the reader meant the key to reach
|
|
157
|
-
what they are writing in.
|
|
158
|
-
|
|
159
|
-
Only a bare printable chord. `mod+k` is a shortcut a reader expects to
|
|
160
|
-
work from inside a search field, and `escape` is how they leave it, so a
|
|
161
|
-
guard over every chord would break the two conventions this is for. It is
|
|
162
|
-
`/` and `?` that are ambiguous, being both the most conventional
|
|
163
|
-
shortcuts on the web and characters somebody types.
|
|
164
|
-
|
|
165
|
-
`composedPath()[0]` rather than `target`, which retargets to the host at
|
|
166
|
-
document level and would need a list of every tag name that holds a field.
|
|
167
|
-
The path's first entry is the real one, through open shadow roots and
|
|
168
|
-
through `delegatesFocus`.
|
|
169
|
-
|
|
170
|
-
**`document` only, because `control` has already answered the question.**
|
|
171
|
-
That binding listens on `this.parentElement`, so it fires only with focus
|
|
172
|
-
inside the control the badge sits in, and the guard would make a bare
|
|
173
|
-
printable chord unreachable there by construction: the two are mutually
|
|
174
|
-
exclusive. `.examples/events.html` is exactly that shape, a `keys="/"`
|
|
175
|
-
slotted into a `helix-input` telling the reader to put the cursor in the
|
|
176
|
-
field and press slash, and it would have shipped to the docs site doing
|
|
177
|
-
nothing. What is ambiguous is `/` competing with every field on the page,
|
|
178
|
-
which is the `document` case and the only one this refuses. */
|
|
179
79
|
if (this.within === 'document' && this.printable && typing(press.composedPath()[0])) {
|
|
180
80
|
return;
|
|
181
81
|
}
|
|
182
82
|
if (!this.matches_(press)) {
|
|
183
83
|
return;
|
|
184
84
|
}
|
|
185
|
-
/* Announced before it is consumed, so a consumer can refuse it: the key
|
|
186
|
-
goes on to whatever would have had it wherever this is prevented. */
|
|
187
85
|
const shortcut = new HelixShortcutEvent({ keys: this.keys });
|
|
188
86
|
this.dispatchEvent(shortcut);
|
|
189
87
|
if (!shortcut.defaultPrevented) {
|
|
@@ -201,15 +99,11 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
|
|
|
201
99
|
}
|
|
202
100
|
updated(changed) {
|
|
203
101
|
super.updated(changed);
|
|
204
|
-
/* The target changes with `within` and the chord changes with `keys`, and
|
|
205
|
-
the second matters as much as the first: a listener holding the old chord
|
|
206
|
-
answers to a key the element no longer advertises. */
|
|
207
102
|
if (changed.has('within') || changed.has('keys')) {
|
|
208
103
|
this.deafen();
|
|
209
104
|
this.listen();
|
|
210
105
|
}
|
|
211
106
|
}
|
|
212
|
-
/** The chord as this reader's keyboard labels it. */
|
|
213
107
|
get label() {
|
|
214
108
|
const names = onApple() ? APPLE : ELSEWHERE;
|
|
215
109
|
const parts = this.keys
|
|
@@ -234,13 +128,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
|
|
|
234
128
|
this.listening?.removeEventListener('keydown', this.handleKeydown);
|
|
235
129
|
this.listening = undefined;
|
|
236
130
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Whether this chord is one bare character, and so indistinguishable from
|
|
239
|
-
* typing.
|
|
240
|
-
*
|
|
241
|
-
* A single part means no modifier, and one code point means a character
|
|
242
|
-
* rather than a name: `/` and `?` are printable, `escape` and `enter` are not.
|
|
243
|
-
*/
|
|
244
131
|
get printable() {
|
|
245
132
|
const parts = this.keys
|
|
246
133
|
.split('+')
|
|
@@ -248,7 +135,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
|
|
|
248
135
|
.filter((part) => part !== '');
|
|
249
136
|
return parts.length === 1 && [...(parts[0] ?? '')].length === 1;
|
|
250
137
|
}
|
|
251
|
-
/** Whether a press is this chord, modifiers and all. */
|
|
252
138
|
matches_(press) {
|
|
253
139
|
const parts = this.keys
|
|
254
140
|
.split('+')
|
|
@@ -260,9 +146,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
|
|
|
260
146
|
const wanted = parts[parts.length - 1] ?? '';
|
|
261
147
|
const modifiers = new Set(parts.slice(0, -1));
|
|
262
148
|
const mod = onApple() ? press.metaKey : press.ctrlKey;
|
|
263
|
-
/* Every modifier is checked in both directions, so `mod+k` is not answered
|
|
264
|
-
by `shift+mod+k`: a chord a reader did not press is a chord that should
|
|
265
|
-
go to whatever did want it. */
|
|
266
149
|
return (press.key.toLowerCase() === wanted &&
|
|
267
150
|
(modifiers.has('mod') ? mod : true) &&
|
|
268
151
|
(modifiers.has('shift') ? press.shiftKey : !press.shiftKey) &&
|
|
@@ -7,67 +7,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, SourceController } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/** How many colors the series palette has, and so where it starts over. */
|
|
11
10
|
const PALETTE = 24;
|
|
12
|
-
/**
|
|
13
|
-
* @summary The names a chart draws, each beside the color it is drawn in.
|
|
14
|
-
*
|
|
15
|
-
* @description
|
|
16
|
-
* A legend is a list, so it is a list: not part of a chart, and shared by
|
|
17
|
-
* whichever of them needs one. It takes its entries from the same table the
|
|
18
|
-
* chart reads, through `for`, so the two cannot disagree about how many series
|
|
19
|
-
* there are or what they are called.
|
|
20
|
-
*
|
|
21
|
-
* `entries` picks which way to read that table, and the two ways are the two
|
|
22
|
-
* shapes a chart comes in. `series` is one entry per data column, named by the
|
|
23
|
-
* header row, which is a line or a bar chart. `categories` is one entry per
|
|
24
|
-
* body row, named by its row header, which is a donut: its slices are the rows,
|
|
25
|
-
* and it has no axis to write their names along.
|
|
26
|
-
*
|
|
27
|
-
* **The table is resolved on the client and there is no server pass.** `for` is
|
|
28
|
-
* an id and resolving it wants a document, which the server render does not
|
|
29
|
-
* have, so a legend ships empty and fills in on hydration. The table itself is
|
|
30
|
-
* ordinary markup and is served whole, so nothing is missing from the page
|
|
31
|
-
* before then, only from this list.
|
|
32
|
-
*
|
|
33
|
-
* The table is watched, because a legend that goes stale when its data changes
|
|
34
|
-
* is a legend that has to be told, and nothing else in the page would know to
|
|
35
|
-
* tell it.
|
|
36
|
-
*
|
|
37
|
-
* **Not hidden from a screen reader, though the mapping it draws is a color
|
|
38
|
-
* one.** The argument for hiding it is real: the colors mean nothing to a
|
|
39
|
-
* reader who cannot see them, and the names are already in the table. It stays
|
|
40
|
-
* because the cost of being wrong runs one way. A duplicated list of three
|
|
41
|
-
* words is noise; a consumer who took the table out of the accessibility tree
|
|
42
|
-
* and then found the legend hidden too has a chart nobody can read, and neither
|
|
43
|
-
* decision announces itself.
|
|
44
|
-
*
|
|
45
|
-
* @since 0.1.0
|
|
46
|
-
* @status experimental
|
|
47
|
-
* @category charts
|
|
48
|
-
* @aliases Key, Series Key
|
|
49
|
-
* @documentation https://helix.fusion.dev/components/legend
|
|
50
|
-
*
|
|
51
|
-
* @csspart entry - One name and its swatch.
|
|
52
|
-
* @csspart list - The list itself.
|
|
53
|
-
* @csspart name - One series name.
|
|
54
|
-
* @csspart swatch - The color a series is drawn in.
|
|
55
|
-
*/
|
|
56
11
|
let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
57
12
|
constructor() {
|
|
58
13
|
super(...arguments);
|
|
59
|
-
/**
|
|
60
|
-
* The id of the table to take the names from.
|
|
61
|
-
*
|
|
62
|
-
* Resolved through `getRootNode()`, so a legend inside another component's
|
|
63
|
-
* shadow root names a table in that root rather than in the document.
|
|
64
|
-
*
|
|
65
|
-
* **In React this arrives as a property and not as an attribute**, the way
|
|
66
|
-
* `Tooltip.for` does, so it does not reflect and nothing styles on it.
|
|
67
|
-
*/
|
|
68
14
|
this.for = '';
|
|
69
|
-
/* One controller rather than the resolve-and-watch each chart would write
|
|
70
|
-
out for itself, which drifts into two names for the same lookup. */
|
|
71
15
|
this.source = new SourceController(this);
|
|
72
16
|
}
|
|
73
17
|
willUpdate(changed) {
|
|
@@ -75,15 +19,6 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
|
75
19
|
this.source.watch();
|
|
76
20
|
}
|
|
77
21
|
}
|
|
78
|
-
/**
|
|
79
|
-
* The names, in the order the chart will draw them.
|
|
80
|
-
*
|
|
81
|
-
* `protected` rather than private because the styled tier has to match a
|
|
82
|
-
* per-series treatment against them, the way `BarChart` matches `outlined` and
|
|
83
|
-
* `subtle` against the headings its table gives. A legend keys a chart, so the
|
|
84
|
-
* two answer the same question about the same series, and the tier that paints
|
|
85
|
-
* cannot do that without knowing which entry is which.
|
|
86
|
-
*/
|
|
87
22
|
names() {
|
|
88
23
|
const series = this.source.series;
|
|
89
24
|
return this.entries === 'categories'
|
|
@@ -91,14 +26,6 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
|
|
|
91
26
|
: series.map((one) => one.name);
|
|
92
27
|
}
|
|
93
28
|
render() {
|
|
94
|
-
/* The index rather than the color, because a color is a token and the
|
|
95
|
-
headless tier holds none. The styled tier maps one to the other, and it
|
|
96
|
-
starts over after twenty-four.
|
|
97
|
-
*
|
|
98
|
-
* The palette is eight hues in three tones, so a legend of nine repeats the
|
|
99
|
-
* first hue in a brighter tone rather than repeating a color outright. It
|
|
100
|
-
* is still a hue collision and a chart with nine series has a worse problem
|
|
101
|
-
* than the palette does; twenty-four is where it stops being able to help. */
|
|
102
29
|
return html `<ul part="list" role="list">
|
|
103
30
|
${this.names().map((name, index) => html `<li
|
|
104
31
|
data-series=${(index % PALETTE) + 1}
|
|
@@ -8,110 +8,28 @@ import { customElement, drawTooltip, extent, HelixElement, linear, linePath, Mix
|
|
|
8
8
|
import { html, nothing, svg } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import '../../tooltip/index.js';
|
|
11
|
-
/** The drawing's own coordinates. */
|
|
12
11
|
const WIDTH = 100;
|
|
13
12
|
const HEIGHT = 100;
|
|
14
|
-
/**
|
|
15
|
-
* @summary A series over time as a line, read from a table.
|
|
16
|
-
*
|
|
17
|
-
* @description
|
|
18
|
-
* The chart for a shape rather than for a quantity, which is what decides the
|
|
19
|
-
* one thing a bar chart is not allowed to do.
|
|
20
|
-
*
|
|
21
|
-
* **It measures from its own range and not from zero, deliberately.** `BarChart`
|
|
22
|
-
* refuses that switch because a bar's length *is* its value, so a baseline
|
|
23
|
-
* anywhere else makes the lengths proportional to nothing. A line claims no
|
|
24
|
-
* such thing: it says where the readings went, and forcing zero into the range
|
|
25
|
-
* of a series that moves between 980 and 1020 flattens the entire shape into a
|
|
26
|
-
* rule across the top of the box. The two rules are opposite because the two
|
|
27
|
-
* charts answer opposite questions, and `zero` is here for the case where the
|
|
28
|
-
* distance from nothing is the point.
|
|
29
|
-
*
|
|
30
|
-
* **One scale across every series**, the same as the bar chart, because two
|
|
31
|
-
* series drawn to different scales are two charts sharing a picture.
|
|
32
|
-
*
|
|
33
|
-
* **The labels are HTML beside the drawing rather than `<text>` inside it**, for
|
|
34
|
-
* the reason `BarChart` records: an `<svg>` that fills its box scales its type
|
|
35
|
-
* with everything else, the same `font-size` arriving at 10px in a narrow chart
|
|
36
|
-
* and 43px in a wide one, and there is no non-scaling-font to answer it the way
|
|
37
|
-
* `vector-effect` answers strokes.
|
|
38
|
-
*
|
|
39
|
-
* The table is the accessible representation and this drawing is `aria-hidden`.
|
|
40
|
-
* A reader who wants the numbers gets the numbers, not a described picture.
|
|
41
|
-
*
|
|
42
|
-
* @since 0.1.0
|
|
43
|
-
* @status experimental
|
|
44
|
-
* @category charts
|
|
45
|
-
* @aliases Time Series, Trend Chart, Multi-line Chart
|
|
46
|
-
* @documentation https://helix.fusion.dev/components/line-chart
|
|
47
|
-
*
|
|
48
|
-
* @csspart area - The wash under one line, where `area` is set.
|
|
49
|
-
* @csspart band - The slot the pointer is resting in.
|
|
50
|
-
* @csspart baseline - The rule at zero, where zero is inside the range.
|
|
51
|
-
* @csspart gridline - One rule across the plot, under `gridlines`.
|
|
52
|
-
* @csspart guide - A rule down the reading a pointer is resting on.
|
|
53
|
-
* @csspart label - One reading's name.
|
|
54
|
-
* @csspart labels - The row of reading names.
|
|
55
|
-
* @csspart line - One series.
|
|
56
|
-
* @csspart mark - The invisible point the tooltip is anchored to.
|
|
57
|
-
* @csspart plot - The drawing.
|
|
58
|
-
* @csspart svg - The drawing surface the lines and areas sit on.
|
|
59
|
-
* @csspart terminus - The mark on one series' last reading.
|
|
60
|
-
* @csspart tooltip - The bubble, forwarded from `Tooltip`.
|
|
61
|
-
* @csspart tooltip-anchor - The `Tooltip` element itself.
|
|
62
|
-
* @csspart tooltip-cell - One column's name and value inside the tooltip.
|
|
63
|
-
* @csspart tooltip-label - The reading's own name inside the tooltip.
|
|
64
|
-
* @csspart tooltip-name - A column's name inside the tooltip.
|
|
65
|
-
* @csspart tooltip-value - A column's value inside the tooltip.
|
|
66
|
-
*/
|
|
67
13
|
let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSource(HelixElement) {
|
|
68
14
|
constructor() {
|
|
69
15
|
super(...arguments);
|
|
70
|
-
/**
|
|
71
|
-
* Whether a wash is drawn under each line.
|
|
72
|
-
*
|
|
73
|
-
* Absent is lines alone, which is what suits more than one series: washes
|
|
74
|
-
* stack and the lower ones are read through the upper. One series over a
|
|
75
|
-
* ground it should feel like a quantity against is the case for this.
|
|
76
|
-
*/
|
|
77
16
|
this.area = false;
|
|
78
|
-
/**
|
|
79
|
-
* Whether zero is forced into the range.
|
|
80
|
-
*
|
|
81
|
-
* Absent measures from the data's own extent, which is what a line is for.
|
|
82
|
-
* Set it where the distance from nothing is the point rather than the shape,
|
|
83
|
-
* and accept that a series which never approaches zero becomes a flat rule
|
|
84
|
-
* near the top of the box.
|
|
85
|
-
*/
|
|
86
17
|
this.zero = false;
|
|
87
18
|
}
|
|
88
|
-
/** Whether the levels are ruled, which is every value but `category`. */
|
|
89
19
|
get rulesValue() {
|
|
90
20
|
return this.gridlines !== undefined && this.gridlines !== 'category';
|
|
91
21
|
}
|
|
92
|
-
/** Whether each reading is ruled, which only two values ask for. */
|
|
93
22
|
get rulesCategory() {
|
|
94
23
|
return this.gridlines === 'both' || this.gridlines === 'category';
|
|
95
24
|
}
|
|
96
|
-
/** The domain every series is drawn against, which the tooltip also needs. */
|
|
97
25
|
range(series) {
|
|
98
26
|
const values = series.flatMap((one) => one.points.map((point) => point.value));
|
|
99
27
|
const [low, high] = extent(values.length > 0 ? values : [0]);
|
|
100
28
|
return this.zero ? [Math.min(0, low), Math.max(0, high)] : [low, high];
|
|
101
29
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Where each series ends, as percentages of the drawing's own box.
|
|
104
|
-
*
|
|
105
|
-
* Percentages rather than user units, for the reason the mark is a `<span>`
|
|
106
|
-
* rather than a `<circle>`: `preserveAspectRatio="none"` stretches the
|
|
107
|
-
* coordinate space, so a length in there means two different things depending
|
|
108
|
-
* on how wide the consumer made the chart. The host's box stretches nothing.
|
|
109
|
-
*/
|
|
110
30
|
ends(series, x, y) {
|
|
111
31
|
return series.flatMap((one, depth) => {
|
|
112
32
|
const last = one.points.at(-1);
|
|
113
|
-
/* A single reading is a point rather than a line, and a mark on the end of
|
|
114
|
-
something with no length is a mark on nothing. */
|
|
115
33
|
if (!last || one.points.length < 2) {
|
|
116
34
|
return [];
|
|
117
35
|
}
|
|
@@ -132,40 +50,13 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
132
50
|
}
|
|
133
51
|
const domain = this.range(series);
|
|
134
52
|
const y = linear(domain, [HEIGHT, 0]);
|
|
135
|
-
/* **A reading sits in the middle of its own slot, not at the edge of the
|
|
136
|
-
box**, which is what makes the names underneath line up with it.
|
|
137
|
-
*
|
|
138
|
-
* Spanning 0 to 100 is the obvious version and is wrong. The labels are a
|
|
139
|
-
* grid of equal columns, the same row the bar chart uses, so each name is
|
|
140
|
-
* centered in its share of the width while the readings sit at the
|
|
141
|
-
* gridlines between those shares: measured, the first name centered at a
|
|
142
|
-
* twelfth of the width with its point at zero. Every label is off by half
|
|
143
|
-
* a slot and the error grows toward the middle.
|
|
144
|
-
*
|
|
145
|
-
* Insetting by half a slot also means a line chart and a bar chart drawn
|
|
146
|
-
* from the same table stack, which is the arrangement anybody comparing a
|
|
147
|
-
* total to its parts wants. The cost is that the line stops short of both
|
|
148
|
-
* edges, and that is the smaller thing to give up. */
|
|
149
53
|
const slot = WIDTH / readings.length;
|
|
150
54
|
const x = linear([0, Math.max(readings.length - 1, 1)], [slot / 2, WIDTH - slot / 2]);
|
|
151
55
|
return html `${this.renderPlot(series, x, y, domain)} ${this.renderLabels(readings)}`;
|
|
152
56
|
}
|
|
153
|
-
/**
|
|
154
|
-
* The lines themselves, behind a seam the styled tier can move.
|
|
155
|
-
*
|
|
156
|
-
* `aria-hidden`, because the table it was read from is the readable version
|
|
157
|
-
* and a screen reader hearing both hears the same numbers twice.
|
|
158
|
-
*/
|
|
159
57
|
renderPlot(series, x, y, domain) {
|
|
160
|
-
/* Drawn only where zero is inside the range. A rule at the foot of a chart
|
|
161
|
-
whose lowest reading is 980 is not the zero line, it is the bottom of the
|
|
162
|
-
box wearing the zero line's clothes, and a reader takes it for one. */
|
|
163
58
|
const floor = domain[0] <= 0 && domain[1] >= 0 ? y(0) : undefined;
|
|
164
59
|
const ends = this.terminus === 'none' ? [] : this.ends(series, x, y);
|
|
165
|
-
/* Built here rather than in the sheet, because a stylesheet cannot count
|
|
166
|
-
series and a mask needs one layer per hole. Each layer is transparent
|
|
167
|
-
inside its own ring and opaque everywhere else, so intersecting them
|
|
168
|
-
removes the union of the holes and nothing besides. */
|
|
169
60
|
const holes = this.terminus === 'ring' && ends.length > 0
|
|
170
61
|
? ends
|
|
171
62
|
.map((end) => `radial-gradient(circle at ${end.x}% ${end.y}%, #0000 calc(var(--_chart-terminus-inner) - 0.5px), #000 var(--_chart-terminus-inner))`)
|
|
@@ -194,22 +85,10 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
194
85
|
${this.renderTooltip()}
|
|
195
86
|
</div>`;
|
|
196
87
|
}
|
|
197
|
-
/**
|
|
198
|
-
* The grid, behind everything, or nothing at all.
|
|
199
|
-
*
|
|
200
|
-
* Drawn first so a series is never cut by a rule, and before the baseline so
|
|
201
|
-
* that where the two coincide the baseline wins: zero is a fact about the
|
|
202
|
-
* data and a gridline is a ruler, and the reader should see the fact.
|
|
203
|
-
*/
|
|
204
88
|
renderGridlines(x, y, domain, count) {
|
|
205
89
|
if (this.gridlines === undefined) {
|
|
206
90
|
return nothing;
|
|
207
91
|
}
|
|
208
|
-
/* Through each reading rather than between them. A rule on the boundary
|
|
209
|
-
divides two catchments, which is a fact about the pointer; a rule on the
|
|
210
|
-
reading is the line a reader traces down to the label, which is what a
|
|
211
|
-
category grid is for. The readings sit at slot centers, so the two are
|
|
212
|
-
half a slot apart and only one of them lands under a point. */
|
|
213
92
|
const down = this.rulesCategory
|
|
214
93
|
? Array.from({ length: count }, (_, index) => svg `<line part="gridline" x1=${x(index)} x2=${x(index)} y1="0" y2=${HEIGHT} />`)
|
|
215
94
|
: [];
|
|
@@ -218,19 +97,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
218
97
|
: [];
|
|
219
98
|
return svg `${down}${across}`;
|
|
220
99
|
}
|
|
221
|
-
/**
|
|
222
|
-
* The slot the pointer is resting in, drawn behind the drawing.
|
|
223
|
-
*
|
|
224
|
-
* A band and not a second rule. The guide says exactly where the reading is
|
|
225
|
-
* and the band says which share of the width belongs to it, which is what a
|
|
226
|
-
* reader is actually pointing at: the catchment is a slot wide, so a pointer
|
|
227
|
-
* a few pixels off the line is still asking about that reading, and without
|
|
228
|
-
* the band nothing on screen says so.
|
|
229
|
-
*
|
|
230
|
-
* The same slot arithmetic the pointer controller resolves with, so the lit
|
|
231
|
-
* region and the region that answers are the same region by construction
|
|
232
|
-
* rather than by two files agreeing.
|
|
233
|
-
*/
|
|
234
100
|
renderBand(count) {
|
|
235
101
|
const at = this.pointer.at;
|
|
236
102
|
if (at < 0 || count === 0) {
|
|
@@ -239,17 +105,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
239
105
|
const slot = WIDTH / count;
|
|
240
106
|
return svg `<rect height=${HEIGHT} part="band" width=${slot} x=${at * slot} y="0" />`;
|
|
241
107
|
}
|
|
242
|
-
/**
|
|
243
|
-
* A rule down the reading the tooltip is about.
|
|
244
|
-
*
|
|
245
|
-
* Without it the tooltip is a box of numbers floating over a drawing, and a
|
|
246
|
-
* reader has to match its heading against the row of names to find out which
|
|
247
|
-
* column it means. The rule is the whole of what connects the two, and it
|
|
248
|
-
* matters most on a line, where there is no marker under the pointer to
|
|
249
|
-
* stand in for one.
|
|
250
|
-
*
|
|
251
|
-
* Drawn before the series so a line is never cut by it.
|
|
252
|
-
*/
|
|
253
108
|
renderGuide(x) {
|
|
254
109
|
const at = this.pointer.at;
|
|
255
110
|
if (at < 0) {
|
|
@@ -257,12 +112,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
257
112
|
}
|
|
258
113
|
return svg `<line part="guide" x1=${x(at)} x2=${x(at)} y1="0" y2=${HEIGHT} />`;
|
|
259
114
|
}
|
|
260
|
-
/**
|
|
261
|
-
* The marks, as elements beside the drawing rather than shapes inside it.
|
|
262
|
-
*
|
|
263
|
-
* A seam of its own, so a tier or a consumer's subclass can draw them
|
|
264
|
-
* differently without reimplementing the plot.
|
|
265
|
-
*/
|
|
266
115
|
renderTermini(ends) {
|
|
267
116
|
if (ends.length === 0) {
|
|
268
117
|
return nothing;
|
|
@@ -273,7 +122,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
273
122
|
style=${`--chart-series: var(--chart-series-${end.index}); --chart-terminus-x: ${end.x}%; --chart-terminus-y: ${end.y}%`}
|
|
274
123
|
></span>`)}`;
|
|
275
124
|
}
|
|
276
|
-
/** One series, its wash first so the line is drawn over its own fill. */
|
|
277
125
|
renderSeries(one, depth, x, y) {
|
|
278
126
|
const points = one.points.map((point, index) => [x(index), y(point.value)]);
|
|
279
127
|
const path = linePath(points, this.curve ?? 'smooth');
|
|
@@ -281,10 +129,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
281
129
|
return html `${nothing}`;
|
|
282
130
|
}
|
|
283
131
|
const index = seriesIndex(depth);
|
|
284
|
-
/* Closed along the floor of the box and back, which is what makes it
|
|
285
|
-
fillable: a `<path>` is filled between its own ends, so an open line
|
|
286
|
-
fills the region between the stroke and the straight line joining its
|
|
287
|
-
first point to its last. */
|
|
288
132
|
const first = points[0];
|
|
289
133
|
const last = points.at(-1);
|
|
290
134
|
const wash = `${path}L${last[0]},${HEIGHT}L${first[0]},${HEIGHT}Z`;
|
|
@@ -303,33 +147,17 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
|
|
|
303
147
|
style="--chart-series: var(--chart-series-${index})"
|
|
304
148
|
/>`;
|
|
305
149
|
}
|
|
306
|
-
/**
|
|
307
|
-
* The reading names, as HTML beside the drawing rather than inside it.
|
|
308
|
-
*
|
|
309
|
-
* Inside, they would scale with the box, for the reason the class doc gives.
|
|
310
|
-
*/
|
|
311
150
|
renderLabels(readings) {
|
|
312
151
|
return html `<div aria-hidden="true" part="labels">
|
|
313
152
|
${readings.map((one) => html `<span part="label">${one}</span>`)}
|
|
314
153
|
</div>`;
|
|
315
154
|
}
|
|
316
|
-
/**
|
|
317
|
-
* What a pointer resting on a reading shows.
|
|
318
|
-
*
|
|
319
|
-
* A seam over `drawTooltip`, which holds the markup every chart shares. The
|
|
320
|
-
* usual reason to override is arrangement: every cell of the row is already
|
|
321
|
-
* there, including the columns nothing was plotted from.
|
|
322
|
-
*/
|
|
323
155
|
renderTooltip() {
|
|
324
156
|
const at = this.pointer.at;
|
|
325
157
|
const series = this.series;
|
|
326
158
|
if (at < 0 || series.length === 0) {
|
|
327
159
|
return drawTooltip(this.sourceRows, at, this.tag('tooltip'));
|
|
328
160
|
}
|
|
329
|
-
/* Above the topmost series at that reading, so the box never covers the
|
|
330
|
-
line it is describing. `y` counts down in the drawing, so the smallest is
|
|
331
|
-
the highest, and no readings at all would make `Math.min` return
|
|
332
|
-
Infinity. */
|
|
333
161
|
const domain = this.range(series);
|
|
334
162
|
const scale = linear(domain, [HEIGHT, 0]);
|
|
335
163
|
const tops = series
|