@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.
- 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,145 +7,39 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing, svg } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary A measurement in a known range, and whether it is where you want it.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* **Not `Progress`, and the difference is the whole component.** A progress bar
|
|
15
|
-
* reports how far along a task is: it starts empty, ends full, and full is
|
|
16
|
-
* always the good end. A meter reports a level in a range that is not going
|
|
17
|
-
* anywhere, and which end is good is a fact about the thing being measured.
|
|
18
|
-
* Disk usage, a battery, a score, a rate limit. So there is no `indeterminate`
|
|
19
|
-
* here: a measurement whose value is unknown is not a measurement.
|
|
20
|
-
*
|
|
21
|
-
* **`low`, `high` and `optimum` are the reason it exists**, and they follow the
|
|
22
|
-
* HTML specification's own rule. The three thresholds cut the range into a low
|
|
23
|
-
* band, a middle and a high band, and `optimum` says which of the three is the
|
|
24
|
-
* one to be in. The band the value falls in is then the good one, the one next
|
|
25
|
-
* to it is fair, and the one two away is poor.
|
|
26
|
-
*
|
|
27
|
-
* **Without thresholds there are no bands, and this is where it parts company
|
|
28
|
-
* with `<meter>`.** The platform defaults `optimum` to the midpoint and both
|
|
29
|
-
* thresholds to the ends, so a bare `<meter value="20" max="100">` computes as
|
|
30
|
-
* optimum and every browser paints it green. A gauge with nothing said about it
|
|
31
|
-
* is not good news; it is a reading. Declared thresholds turn the color on.
|
|
32
|
-
*
|
|
33
|
-
* **And the bands are drawn as well as colored.** A tick sits at `low` and at
|
|
34
|
-
* `high`, so the reading can be placed against them without seeing color at
|
|
35
|
-
* all, which is the same call the carousel's current dot makes by lengthening
|
|
36
|
-
* rather than only changing color.
|
|
37
|
-
*
|
|
38
|
-
* The properties below are written by this component and read by whoever draws
|
|
39
|
-
* it. A consumer sets none of them: they carry a number this element computed
|
|
40
|
-
* and CSS cannot, which is what lets a stylesheet on this tier draw the thing
|
|
41
|
-
* without measuring anything itself.
|
|
42
|
-
*
|
|
43
|
-
* @since 0.1.0
|
|
44
|
-
* @status stable
|
|
45
|
-
* @category feedback
|
|
46
|
-
* @aliases Gauge, Level, Capacity
|
|
47
|
-
* @documentation https://helix.fusion.dev/components/meter
|
|
48
|
-
*
|
|
49
|
-
* @csspart groove - The unfilled circle, in `ring` and `arc`.
|
|
50
|
-
* @csspart indicator - The part of it the value fills.
|
|
51
|
-
* @csspart tick - A mark at `low` or at `high`.
|
|
52
|
-
* @csspart track - The whole range, or the drawing in `ring` and `arc`.
|
|
53
|
-
* @csspart value - The reading, under `show-value`.
|
|
54
|
-
*
|
|
55
|
-
* @cssproperty --meter-along - Where a bound sits on the arc, as a length along it.
|
|
56
|
-
* @cssproperty --meter-at - Where a bound sits on a linear track, as a percentage.
|
|
57
|
-
* @cssproperty --meter-fraction - The value as a fraction of the range, 0 to 1.
|
|
58
|
-
* @cssproperty --meter-sweep - How much of the circle the arc covers, as a path length.
|
|
59
|
-
*/
|
|
60
10
|
let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
|
|
61
11
|
constructor() {
|
|
62
12
|
super(...arguments);
|
|
63
|
-
/**
|
|
64
|
-
* Accessible name for the reading.
|
|
65
|
-
*/
|
|
66
13
|
this.label = 'Meter';
|
|
67
|
-
/**
|
|
68
|
-
* Top of the range.
|
|
69
|
-
*
|
|
70
|
-
* A hundred rather than the platform's one, which is what `Progress` already
|
|
71
|
-
* takes and what a consumer writing `value="72"` means. A range of zero to one
|
|
72
|
-
* is a fraction, and a component that reads `value="72"` as seventy-two times
|
|
73
|
-
* over its maximum is a trap that costs nothing to avoid.
|
|
74
|
-
*/
|
|
75
14
|
this.max = 100;
|
|
76
|
-
/**
|
|
77
|
-
* Bottom of the range.
|
|
78
|
-
*/
|
|
79
15
|
this.min = 0;
|
|
80
|
-
/**
|
|
81
|
-
* Whether the reading is shown beside the bar.
|
|
82
|
-
*/
|
|
83
16
|
this.showValue = false;
|
|
84
|
-
/**
|
|
85
|
-
* Where the reading sits between `min` and `max`.
|
|
86
|
-
*
|
|
87
|
-
* Outside them, it clamps.
|
|
88
|
-
*/
|
|
89
17
|
this.value = 0;
|
|
90
|
-
/**
|
|
91
|
-
* What the reading says, where a number is not the whole story.
|
|
92
|
-
*
|
|
93
|
-
* `value-text="18 GB of 32 GB"` is announced and shown instead of `56%`. The
|
|
94
|
-
* same property `Progress` carries, for the same reason: a percentage is a
|
|
95
|
-
* fair summary of a task and a poor one of a quantity.
|
|
96
|
-
*/
|
|
97
18
|
this.valueText = '';
|
|
98
19
|
}
|
|
99
|
-
/** The range, clamped so `max` is never at or below `min`. */
|
|
100
20
|
get span() {
|
|
101
21
|
const min = Number.isFinite(this.min) ? this.min : 0;
|
|
102
22
|
const max = Number.isFinite(this.max) && this.max > min ? this.max : min + 1;
|
|
103
23
|
return { max, min };
|
|
104
24
|
}
|
|
105
|
-
/** The reading, pulled into the range. */
|
|
106
25
|
get reading() {
|
|
107
26
|
const { max, min } = this.span;
|
|
108
27
|
return Math.min(max, Math.max(min, Number.isFinite(this.value) ? this.value : min));
|
|
109
28
|
}
|
|
110
|
-
/**
|
|
111
|
-
* How much of the track is filled, from zero to one.
|
|
112
|
-
*
|
|
113
|
-
* Clamped, because a bar cannot draw past its own end. What the reading *says*
|
|
114
|
-
* is not clamped: see `ratio`.
|
|
115
|
-
*/
|
|
116
29
|
get fraction() {
|
|
117
30
|
const { max, min } = this.span;
|
|
118
31
|
return (this.reading - min) / (max - min);
|
|
119
32
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Where the value really sits, which is what the words report.
|
|
122
|
-
*
|
|
123
|
-
* **Unclamped, so a value past the end is not swallowed.** Clamped as well as
|
|
124
|
-
* the bar is, 110 and 250 read as exactly full, and a meter that cannot say
|
|
125
|
-
* "you are over" is worse than no meter for the one case anybody checks it in.
|
|
126
|
-
*
|
|
127
|
-
* The bar still stops at its end, because that is what an end is. The words
|
|
128
|
-
* and the accessible reading carry the truth, and `data-beyond` is on the fill
|
|
129
|
-
* so a consumer can say more about it.
|
|
130
|
-
*/
|
|
131
33
|
get ratio() {
|
|
132
34
|
const { max, min } = this.span;
|
|
133
35
|
const raw = Number.isFinite(this.value) ? this.value : min;
|
|
134
36
|
return (raw - min) / (max - min);
|
|
135
37
|
}
|
|
136
|
-
/** Whether the value is past an end, which the bar itself cannot show. */
|
|
137
38
|
get beyond() {
|
|
138
39
|
const { max, min } = this.span;
|
|
139
40
|
const raw = Number.isFinite(this.value) ? this.value : min;
|
|
140
41
|
return raw > max ? 'over' : raw < min ? 'under' : undefined;
|
|
141
42
|
}
|
|
142
|
-
/**
|
|
143
|
-
* The thresholds, in order, or nothing where the consumer declared none.
|
|
144
|
-
*
|
|
145
|
-
* Both are clamped into the range and to each other, because a `low` above a
|
|
146
|
-
* `high` describes no bands at all and the spec's own answer is to raise the
|
|
147
|
-
* one that is out of order rather than to give up.
|
|
148
|
-
*/
|
|
149
43
|
get thresholds() {
|
|
150
44
|
const { max, min } = this.span;
|
|
151
45
|
if (this.low === undefined && this.high === undefined) {
|
|
@@ -155,14 +49,6 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
|
|
|
155
49
|
const high = Math.min(max, Math.max(low, this.high ?? max));
|
|
156
50
|
return { high, low };
|
|
157
51
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Which band the reading is in, by the specification's rule.
|
|
160
|
-
*
|
|
161
|
-
* `optimum` names a point, and the band containing it is the good one. A
|
|
162
|
-
* reading in that band is good, one in a band beside it is fair, and one two
|
|
163
|
-
* bands away is poor. Undefined where nothing was declared, which is what
|
|
164
|
-
* keeps an unbanded meter from being painted as good news.
|
|
165
|
-
*/
|
|
166
52
|
get computedBand() {
|
|
167
53
|
if (this.band) {
|
|
168
54
|
return this.band;
|
|
@@ -177,47 +63,19 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
|
|
|
177
63
|
const distance = Math.abs(which(this.reading) - which(optimum));
|
|
178
64
|
return distance === 0 ? 'good' : distance === 1 ? 'fair' : 'poor';
|
|
179
65
|
}
|
|
180
|
-
/** What the reading says, which is the consumer's words or a percentage. */
|
|
181
66
|
get text() {
|
|
182
67
|
return this.valueText || `${Math.round(this.ratio * 100)}%`;
|
|
183
68
|
}
|
|
184
69
|
willUpdate(changed) {
|
|
185
70
|
super.willUpdate(changed);
|
|
186
71
|
const { max, min } = this.span;
|
|
187
|
-
/* On the host through internals, so the reading is the element itself and
|
|
188
|
-
an author's own `aria-label` beats it without anything being written onto
|
|
189
|
-
markup their server never had. `meter` is the ARIA role for exactly this
|
|
190
|
-
and is not `progressbar`: a progress bar promises it is going somewhere. */
|
|
191
72
|
hostAria(this.internals, { label: this.label, role: 'meter' });
|
|
192
73
|
this.internals.ariaValueMin = String(min);
|
|
193
74
|
this.internals.ariaValueMax = String(max);
|
|
194
75
|
this.internals.ariaValueNow = String(this.reading);
|
|
195
|
-
/* The consumer's words where they gave any, and otherwise nothing at all
|
|
196
|
-
*unless* the value is past an end. `ariaValueNow` has to sit inside the
|
|
197
|
-
range, so it is the only place a reader can be told that 250 is not 100. */
|
|
198
76
|
this.internals.ariaValueText =
|
|
199
77
|
this.valueText || (this.beyond ? `${Math.round(this.ratio * 100)}%` : null);
|
|
200
78
|
}
|
|
201
|
-
/**
|
|
202
|
-
* The same reading, drawn round.
|
|
203
|
-
*
|
|
204
|
-
* `pathLength="100"` is what makes every number here a percentage: the path is
|
|
205
|
-
* declared a hundred units long, so nothing computes a circumference, a radius
|
|
206
|
-
* or pi, and a consumer changing the thickness moves the circle that carries
|
|
207
|
-
* it without this element recalculating anything. Progress does the same and
|
|
208
|
-
* for the same reason.
|
|
209
|
-
*
|
|
210
|
-
* The sweep is what separates the two shapes. A ring is the whole circle and
|
|
211
|
-
* an arc is three quarters of it, open at the bottom, which is where a gauge
|
|
212
|
-
* has always had its gap: the two ends need somewhere to be that is not each
|
|
213
|
-
* other, and the opening is where a reading of the value goes.
|
|
214
|
-
*
|
|
215
|
-
* Ticks are segments of the same circle rather than marks placed on it. One
|
|
216
|
-
* unit of a hundred, offset to where the threshold falls, which needs no
|
|
217
|
-
* trigonometry and stays correct at any radius or thickness. Placing them by
|
|
218
|
-
* hand would mean sine and cosine in a template and a second definition of
|
|
219
|
-
* where the arc starts.
|
|
220
|
-
*/
|
|
221
79
|
renderRound() {
|
|
222
80
|
const bounds = this.thresholds;
|
|
223
81
|
const { max, min } = this.span;
|
|
@@ -225,16 +83,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
|
|
|
225
83
|
const along = (value) => ((value - min) / (max - min)) * sweep;
|
|
226
84
|
return html `<svg part="track" viewBox="0 0 100 100" aria-hidden="true">
|
|
227
85
|
<circle part="groove" pathLength="100" style="--meter-sweep: ${sweep}" />
|
|
228
|
-
${
|
|
229
|
-
/* `svg` and not `html`, which is the bug the sparkline records one
|
|
230
|
-
file over. A nested Lit template is parsed on its own, so a
|
|
231
|
-
`<circle>` written in one has no `<svg>` around it and lands in the
|
|
232
|
-
XHTML namespace: it takes the part, takes the style, inherits every
|
|
233
|
-
rule in the stylesheet, and draws nothing, because geometry belongs
|
|
234
|
-
to the namespace and not to the tag name. Both threshold ticks were
|
|
235
|
-
invisible in `shape="ring"` and `shape="arc"`, which is the whole of
|
|
236
|
-
what the round example shows. */
|
|
237
|
-
bounds
|
|
86
|
+
${bounds
|
|
238
87
|
? svg `<circle
|
|
239
88
|
part="tick"
|
|
240
89
|
pathLength="100"
|
|
@@ -7,90 +7,34 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, IndicatorController } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary A set of destinations, with a marker on the current one.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* Renders a real `<nav>`, which is the landmark a screen-reader user jumps to.
|
|
15
|
-
* It exists as its own component rather than as a region of `AppHeader` because
|
|
16
|
-
* a header is not inherently navigation, and navigation is not only ever in a
|
|
17
|
-
* header: a footer and a sidebar hold the same thing.
|
|
18
|
-
*
|
|
19
|
-
* **It renders no links.** The consumer brings them, marked with
|
|
20
|
-
* `aria-current="page"` and styled by the `data-navigation-item` recipe, for the same
|
|
21
|
-
* reason nothing else here renders an anchor: a router renders its own, and
|
|
22
|
-
* cannot render it inside this shadow root.
|
|
23
|
-
*
|
|
24
|
-
* What this owns is the marker that travels between them, and that is the whole
|
|
25
|
-
* reason it is a component. A marker that moves needs something that knows
|
|
26
|
-
* where every item is, and reading `aria-current` is how it knows which: the
|
|
27
|
-
* thing that makes the page announce correctly is the same thing that puts the
|
|
28
|
-
* marker in the right place.
|
|
29
|
-
*
|
|
30
|
-
* @since 0.1.0
|
|
31
|
-
* @status experimental
|
|
32
|
-
* @category navigation
|
|
33
|
-
* @aliases Nav, NavList, Tabs
|
|
34
|
-
* @documentation https://helix.fusion.dev/components/navigation
|
|
35
|
-
*
|
|
36
|
-
* @slot - The destinations, as the consumer's own links.
|
|
37
|
-
*
|
|
38
|
-
* @csspart indicator - The marker that travels to the current destination.
|
|
39
|
-
* @csspart nav - The navigation landmark.
|
|
40
|
-
*/
|
|
41
10
|
let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement {
|
|
42
11
|
constructor() {
|
|
43
12
|
super(...arguments);
|
|
44
|
-
/* The controller carries the RTL sign and border correction: a distance
|
|
45
|
-
measured from the physical left and applied from the inline-start edge
|
|
46
|
-
travels the wrong way in a right-to-left document. */
|
|
47
13
|
this.indicator = new IndicatorController(this, {
|
|
48
14
|
active: () => this.items.find((item) => item.getAttribute('aria-current') === 'page'),
|
|
49
15
|
indicator: () => this.renderRoot?.querySelector('[part="indicator"]'),
|
|
50
16
|
surface: () => this.renderRoot?.querySelector('[part="nav"]'),
|
|
51
17
|
});
|
|
52
|
-
/**
|
|
53
|
-
* Accessible name for the landmark.
|
|
54
|
-
*/
|
|
55
18
|
this.label = 'Main';
|
|
56
19
|
}
|
|
57
|
-
/** The destinations the consumer slotted. */
|
|
58
20
|
get items() {
|
|
59
21
|
const slot = this.renderRoot.querySelector('slot');
|
|
60
22
|
return (slot?.assignedElements() ?? []);
|
|
61
23
|
}
|
|
62
|
-
/** Re-measures when the links change, move, or hand over the current page. */
|
|
63
24
|
watch() {
|
|
64
25
|
this.observer?.disconnect();
|
|
65
26
|
this.current?.disconnect();
|
|
66
|
-
/* Nothing is written onto the items, and `data-navigation-item` must not be
|
|
67
|
-
* written here: it would land after upgrade, so a server-rendered nav would
|
|
68
|
-
* arrive unstyled and restyle itself a frame later, and a React consumer
|
|
69
|
-
* who wrote the attribute themselves gets `data-navigation-item="true"`
|
|
70
|
-
* from JSX, which overwriting reports as a hydration mismatch. The base
|
|
71
|
-
* layer selects `helix-navigation a`, so the styling is true the moment the
|
|
72
|
-
* markup is parsed, on the server and on the client. */
|
|
73
27
|
this.indicator.place();
|
|
74
28
|
this.observer = new ResizeObserver(() => this.indicator.place());
|
|
75
29
|
this.observer.observe(this);
|
|
76
30
|
for (const item of this.items) {
|
|
77
31
|
this.observer.observe(item);
|
|
78
32
|
}
|
|
79
|
-
// Client-side routing changes `aria-current` in place rather than
|
|
80
|
-
// reloading, so nothing else would tell the marker to move.
|
|
81
33
|
this.current = new MutationObserver(() => this.indicator.place());
|
|
82
34
|
for (const item of this.items) {
|
|
83
35
|
this.current.observe(item, { attributeFilter: ['aria-current'] });
|
|
84
36
|
}
|
|
85
37
|
}
|
|
86
|
-
/* Rebuilt on reconnection, because nothing else does it.
|
|
87
|
-
*
|
|
88
|
-
* `firstUpdated` runs once for the life of the element and
|
|
89
|
-
* `disconnectedCallback` runs every time it moves, so a framework reordering
|
|
90
|
-
* a list left this permanently dead: the observer gone, nothing to rebuild
|
|
91
|
-
* it, and the component still rendering as though it were watching. Guarded
|
|
92
|
-
* on `hasUpdated`, because the first connection happens before the first
|
|
93
|
-
* render and there is nothing to watch yet. */
|
|
94
38
|
connectedCallback() {
|
|
95
39
|
super.connectedCallback();
|
|
96
40
|
if (this.hasUpdated) {
|
|
@@ -9,110 +9,18 @@ import { x } from '@fusion.dev/helix-icons';
|
|
|
9
9
|
import { nothing } from 'lit';
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
-
/* `lit/static-html.js`, because this template interpolates a tag name and the
|
|
13
|
-
plain `html` cannot: it renders the placeholder as the element instead, which
|
|
14
|
-
shifts every attribute after it by one. See `renderTime`. */
|
|
15
12
|
import { html } from 'lit/static-html.js';
|
|
16
|
-
/* Side-effect import, because this template interpolates the family's tag and a
|
|
17
|
-
custom element registers on import. Both tiers import it, the relative path
|
|
18
|
-
resolving to each tier's own element. */
|
|
19
13
|
import '../../format-date/index.js';
|
|
20
|
-
/**
|
|
21
|
-
* @summary One thing that happened, which a reader may not have seen yet.
|
|
22
|
-
*
|
|
23
|
-
* @description
|
|
24
|
-
* A notification is a toast that did not go away. `helix-toast` is deliberately
|
|
25
|
-
* the opposite of this: a live region whose items carry a `duration` and whose
|
|
26
|
-
* root never creates or destroys anything. What makes this its own family is the
|
|
27
|
-
* state each item carries rather than the arrangement it sits in: read and
|
|
28
|
-
* unread, a timestamp that has to stay correct, and an action a reader can take
|
|
29
|
-
* later.
|
|
30
|
-
*
|
|
31
|
-
* **The row is a label, a caption under it, and one trailing column that holds
|
|
32
|
-
* either the time or the controls.** A feed is scanned rather than read, so the
|
|
33
|
-
* thing that happened gets the first line on its own and everything qualifying
|
|
34
|
-
* it goes under. The time and the controls occupy the same place because a
|
|
35
|
-
* reader wants exactly one of them at a time: the time while they are scanning,
|
|
36
|
-
* and what they can do about it once they have stopped on a row.
|
|
37
|
-
*
|
|
38
|
-
* **The timestamp is `helix-format-date`**, which is where the reading, the
|
|
39
|
-
* `<time datetime>` and the shared timer now live. It was implemented here, and
|
|
40
|
-
* a component that formats a date is not a fact about notifications: Chat wanted
|
|
41
|
-
* the same thing and had its own copy of half of it.
|
|
42
|
-
*
|
|
43
|
-
* **Read state is reported, not invented.** Marking something read is usually a
|
|
44
|
-
* round trip, and a component that flips its own state and then hears the
|
|
45
|
-
* request failed has told the reader something untrue. `helix-read` is
|
|
46
|
-
* cancelable, and a `read` set by the consumer makes this element defer entirely.
|
|
47
|
-
*
|
|
48
|
-
* @since 0.1.0
|
|
49
|
-
* @status experimental
|
|
50
|
-
* @category feedback
|
|
51
|
-
* @aliases Notification item, Inbox item
|
|
52
|
-
* @documentation https://helix.fusion.dev/components/notifications
|
|
53
|
-
*
|
|
54
|
-
* @slot - What happened, in the consumer's own markup.
|
|
55
|
-
* @slot actions - What a reader can do about it.
|
|
56
|
-
* @slot caption - A second line qualifying it, quieter than the first. A bare
|
|
57
|
-
* slot rather than a box, so an item without one pays nothing for it; style it
|
|
58
|
-
* through `::slotted()`.
|
|
59
|
-
* @slot media - An avatar or icon identifying who or what it came from.
|
|
60
|
-
*
|
|
61
|
-
* @event {HelixDismissEvent} helix-dismiss - Fired when a reader asks to remove it, and cancelable.
|
|
62
|
-
* @event {HelixReadEvent} helix-read - Fired when the item is marked read or unread, and cancelable.
|
|
63
|
-
*
|
|
64
|
-
* @csspart actions - The trailing slot's box.
|
|
65
|
-
* @csspart body - The label and the caption together.
|
|
66
|
-
* @csspart controls - The actions and the dismiss, which replace the time.
|
|
67
|
-
* @csspart dismiss - The control that removes it.
|
|
68
|
-
* @csspart dismiss-mark - The glyph on that control, where none is slotted.
|
|
69
|
-
* @csspart item - The whole notification.
|
|
70
|
-
* @csspart label - What happened, on the first line.
|
|
71
|
-
* @csspart media - The leading slot's box.
|
|
72
|
-
* @csspart time - When it happened, read the way a person reads one.
|
|
73
|
-
* @csspart trailing - The column holding the time and the controls.
|
|
74
|
-
*/
|
|
75
14
|
let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixElement {
|
|
76
15
|
constructor() {
|
|
77
16
|
super(...arguments);
|
|
78
|
-
/**
|
|
79
|
-
* When it happened, as anything `Date` parses.
|
|
80
|
-
*
|
|
81
|
-
* An ISO string is what an API sends and what `<time datetime>` takes, so it
|
|
82
|
-
* is the shape that needs no translation at either end. Left unset the item
|
|
83
|
-
* shows no timestamp rather than showing the epoch, which is what parsing an
|
|
84
|
-
* empty string would give.
|
|
85
|
-
*/
|
|
86
17
|
this.at = '';
|
|
87
|
-
/**
|
|
88
|
-
* State to open with, where nothing declared one.
|
|
89
|
-
*/
|
|
90
18
|
this.defaultRead = false;
|
|
91
|
-
/**
|
|
92
|
-
* Whether a reader may remove it.
|
|
93
|
-
*/
|
|
94
19
|
this.dismissible = false;
|
|
95
|
-
/**
|
|
96
|
-
* Accessible name for the dismiss control.
|
|
97
|
-
*/
|
|
98
20
|
this.dismissLabel = 'Dismiss';
|
|
99
|
-
/**
|
|
100
|
-
* Locale for the timestamp.
|
|
101
|
-
*
|
|
102
|
-
* Empty is the runtime's own.
|
|
103
|
-
*/
|
|
104
21
|
this.locale = '';
|
|
105
|
-
/**
|
|
106
|
-
* @internal Read by the list to identify its children across the light DOM,
|
|
107
|
-
* which is cheaper than `instanceof` and is why this is public rather than
|
|
108
|
-
* protected. Not a consumer's to read or set.
|
|
109
|
-
*/
|
|
110
22
|
this.isNotification = true;
|
|
111
23
|
this.readState = undefined;
|
|
112
|
-
/* Content arriving from the light DOM changes no property here, so nothing
|
|
113
|
-
would schedule the re-read. Bound to the slot rather than to the host,
|
|
114
|
-
because `slotchange` bubbles and is not composed: it reaches the shadow root
|
|
115
|
-
that owns the slot and stops there. */
|
|
116
24
|
this.handleActions = () => {
|
|
117
25
|
this.markControls();
|
|
118
26
|
};
|
|
@@ -123,21 +31,6 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
|
|
|
123
31
|
}
|
|
124
32
|
};
|
|
125
33
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Whether the reader has seen it.
|
|
128
|
-
*
|
|
129
|
-
* Set it and this element defers: a reader's click fires `helix-read` and
|
|
130
|
-
* changes nothing until a new value arrives. `default-read` seeds a state
|
|
131
|
-
* this element then owns.
|
|
132
|
-
*
|
|
133
|
-
* **Not reflected.** Unread is a visual state, which is why the four mixins
|
|
134
|
-
* mirror theirs, but the mirror may not be the property's own attribute:
|
|
135
|
-
* `<helix-notification default-read>` comes back from a server without `read`
|
|
136
|
-
* and would gain it on the first update, which React reports as a hydration
|
|
137
|
-
* mismatch, and which a copy of the serialized element re-parses as a
|
|
138
|
-
* declaration. `:state(read)` is what the stylesheet reads.
|
|
139
|
-
* `hydration.dom.spec.ts` holds it.
|
|
140
|
-
*/
|
|
141
34
|
get read() {
|
|
142
35
|
return this.readState ?? this.defaultRead;
|
|
143
36
|
}
|
|
@@ -146,13 +39,6 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
|
|
|
146
39
|
this.readState = next;
|
|
147
40
|
this.requestUpdate('read', previous);
|
|
148
41
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Asks to be marked read, and reports whether the request stood.
|
|
151
|
-
*
|
|
152
|
-
* Public because a consumer's own control is as likely to ask as this
|
|
153
|
-
* element's own markup is, and because a list marking everything read has to
|
|
154
|
-
* go through the same request rather than writing the property behind it.
|
|
155
|
-
*/
|
|
156
42
|
markRead(next = true) {
|
|
157
43
|
if (next === this.read) {
|
|
158
44
|
return false;
|
|
@@ -171,48 +57,16 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
|
|
|
171
57
|
willUpdate(changed) {
|
|
172
58
|
super.willUpdate(changed);
|
|
173
59
|
hostAria(this.internals, { role: 'listitem' });
|
|
174
|
-
/* The mirror the stylesheet selects on, kept off the host's attributes for
|
|
175
|
-
the reason on `read` itself. */
|
|
176
60
|
this.toggleState('read', this.read);
|
|
177
61
|
}
|
|
178
62
|
updated(changed) {
|
|
179
63
|
super.updated(changed);
|
|
180
64
|
this.markControls();
|
|
181
65
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Says whether there is anything for the time to make way for.
|
|
184
|
-
*
|
|
185
|
-
* The trailing column holds the time and the controls in one place and shows
|
|
186
|
-
* one of them, so a row with nothing to do about it would hide its timestamp
|
|
187
|
-
* on hover and put nothing there. `dismissible` this element knows on the
|
|
188
|
-
* server; whether a consumer filled the actions slot it can only find out from
|
|
189
|
-
* a DOM, so this runs after the render and the resting answer, no controls, is
|
|
190
|
-
* the one the server writes.
|
|
191
|
-
*/
|
|
192
66
|
markControls() {
|
|
193
67
|
const slot = this.renderRoot?.querySelector('slot[name="actions"]');
|
|
194
68
|
this.toggleState('controls', this.dismissible || slotFilled(slot));
|
|
195
69
|
}
|
|
196
|
-
/**
|
|
197
|
-
* The control that removes the item, with the mark a consumer can replace.
|
|
198
|
-
*
|
|
199
|
-
* **A method rather than markup inside `render`, and the slot moves with
|
|
200
|
-
* it.** A fallback answers a consumer and a seam answers the tier above, which
|
|
201
|
-
* are different people: the styled tier wants an icon here, and with the slot
|
|
202
|
-
* written inline the only way to change it is to reimplement the whole
|
|
203
|
-
* template. Moving only the fallback out is not enough: the slot is the thing
|
|
204
|
-
* a tier redraws.
|
|
205
|
-
*
|
|
206
|
-
* **The mark is drawn rather than typed, which is what lets it align.** A
|
|
207
|
-
* character such as `×` is placed by its font rather than by its box:
|
|
208
|
-
* measured against the ghost button beside it at 8x, the cross carried 6
|
|
209
|
-
* pixels of ink to the label's 9.5 and sat 1.5 pixels below its center, in two
|
|
210
|
-
* boxes whose own centers agreed to a tenth of a pixel. No stylesheet can
|
|
211
|
-
* correct that, because the offset is inside the glyph. An `<svg>` is centered
|
|
212
|
-
* on its own viewBox, so it lands where the box says and takes the icon step
|
|
213
|
-
* the row is already set in. It is the `x` the Tag's dismiss draws, so two
|
|
214
|
-
* controls that mean "take this away" are one mark.
|
|
215
|
-
*/
|
|
216
70
|
renderDismiss() {
|
|
217
71
|
if (!this.dismissible) {
|
|
218
72
|
return nothing;
|
|
@@ -236,19 +90,6 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
|
|
|
236
90
|
</slot>
|
|
237
91
|
</button>`;
|
|
238
92
|
}
|
|
239
|
-
/**
|
|
240
|
-
* When it happened, delegated whole.
|
|
241
|
-
*
|
|
242
|
-
* The tag is interpolated rather than written, so each tier renders its own:
|
|
243
|
-
* the base tier may not name a styled element, and a styled item inherits this
|
|
244
|
-
* template without having a tag of its own to remind anybody.
|
|
245
|
-
*
|
|
246
|
-
* `exportparts` and no `part` of its own, which is the rule about a part
|
|
247
|
-
* already named for its role: the child publishes `time`, the role here is
|
|
248
|
-
* `time`, and naming both would leave `::part(time)` matching two elements.
|
|
249
|
-
* What a consumer wants to reach is the `<time>`, so that is what the name
|
|
250
|
-
* reaches.
|
|
251
|
-
*/
|
|
252
93
|
renderTime() {
|
|
253
94
|
const stamp = this.tag('format-date');
|
|
254
95
|
return html `<${stamp}
|