@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
|
@@ -9,89 +9,10 @@ import { x } from '@fusion.dev/helix-icons';
|
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
-
/**
|
|
13
|
-
* @summary One message in the toast region.
|
|
14
|
-
*
|
|
15
|
-
* @description
|
|
16
|
-
* An element rather than a slot, because it carries a timeout, a tone, a
|
|
17
|
-
* dismiss control and a lifetime that pauses: behavior, state and API, which
|
|
18
|
-
* is the test for whether a region becomes an element in this library.
|
|
19
|
-
*
|
|
20
|
-
* **The timer is the item's own.** Held by the region, one message arriving
|
|
21
|
-
* would restart another's, so a stack of three would go from three seconds each
|
|
22
|
-
* to nine for the last one. Each message is on its own clock from the moment it
|
|
23
|
-
* is rendered.
|
|
24
|
-
*
|
|
25
|
-
* **A message is the opposite of the page it is over.** Inverted by default,
|
|
26
|
-
* because a thing arriving in a corner has to be told apart from the page in
|
|
27
|
-
* the moment before it is read, and inverting does that without spending a
|
|
28
|
-
* color that means something. It is a flip of `color-scheme` rather than a
|
|
29
|
-
* named inverse surface: every token in the subtree re-resolves, so a `Retry`
|
|
30
|
-
* a consumer slots in comes out right without knowing where it is. `Tooltip`
|
|
31
|
-
* can name the inverse surface directly because it holds no controls; this
|
|
32
|
-
* cannot. Set `data-color-mode` on the region to take the page's mode instead.
|
|
33
|
-
*
|
|
34
|
-
* **No duration means it stays.** That is the safe default rather than the
|
|
35
|
-
* common one: anything carrying an action has to still be there when the reader
|
|
36
|
-
* decides to take it, and a component cannot tell from the outside whether the
|
|
37
|
-
* message it was given is disposable.
|
|
38
|
-
*
|
|
39
|
-
* **The clock stops while it is being read.** Hover pauses it, and so does
|
|
40
|
-
* focus landing anywhere inside, which is the case that matters more: a
|
|
41
|
-
* keyboard reader tabbing towards the action in a toast is exactly the person a
|
|
42
|
-
* timer must not race. Both resume where they left off rather than starting
|
|
43
|
-
* again, so a message half-read does not get a full second life every time a
|
|
44
|
-
* pointer crosses it.
|
|
45
|
-
*
|
|
46
|
-
* **Going away is a request, never an act.** It dispatches a cancelable
|
|
47
|
-
* `helix-dismiss` and does nothing else: the consumer owns the list, so the
|
|
48
|
-
* item removing itself would leave the two disagreeing. Auto-dismiss and the
|
|
49
|
-
* dismiss control raise the same event, so a consumer handles one path and not
|
|
50
|
-
* two.
|
|
51
|
-
*
|
|
52
|
-
* @since 0.1.0
|
|
53
|
-
* @status experimental
|
|
54
|
-
* @documentation https://helix.fusion.dev/components/toast
|
|
55
|
-
*
|
|
56
|
-
* @slot - The message.
|
|
57
|
-
* @slot actions - What to do about it, usually a button or a link.
|
|
58
|
-
* @slot dismiss-icon - Replaces the glyph inside the dismiss control.
|
|
59
|
-
* @slot icon - Replaces the tone mark. Matches `Callout`, which is why the
|
|
60
|
-
* dismiss control's own glyph is `dismiss-icon` rather than this: two slots
|
|
61
|
-
* cannot share a name, assignment goes to whichever comes first in the tree,
|
|
62
|
-
* and the tone mark is the one a consumer reaches for.
|
|
63
|
-
*
|
|
64
|
-
* @event {HelixDismissEvent} helix-dismiss - Fired when it asks to go, whether by the control or by its timer. Cancelable: refusing it takes the decision and nothing moves.
|
|
65
|
-
*
|
|
66
|
-
* @csspart actions - The region holding what to do about it.
|
|
67
|
-
* @csspart content - The message.
|
|
68
|
-
* @csspart dismiss - The control that asks to close.
|
|
69
|
-
* @csspart dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
70
|
-
* @csspart icon - The tone mark, where the message has one.
|
|
71
|
-
* @csspart icon-mark - The tone glyph, where none is slotted.
|
|
72
|
-
* @csspart item - The message's own surface.
|
|
73
|
-
* @csspart timer - The bar showing how long is left, where a duration was given.
|
|
74
|
-
*
|
|
75
|
-
* @cssproperty --toast-duration - How long this message stays, in milliseconds, for a progress hint.
|
|
76
|
-
*/
|
|
77
12
|
let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissible(HelixElement) {
|
|
78
13
|
constructor() {
|
|
79
14
|
super(...arguments);
|
|
80
|
-
/** Whether it is showing, whoever said so, and `undefined` until somebody has. */
|
|
81
15
|
this.openState = undefined;
|
|
82
|
-
/* Arrows, so `this` is the item wherever the listener ends up. Lit does bind
|
|
83
|
-
a template listener to its host, and relying on that puts a load-bearing
|
|
84
|
-
fact about a library in a place nobody reading this file would look. */
|
|
85
|
-
/**
|
|
86
|
-
* How many reasons there are to hold the clock, rather than whether there is
|
|
87
|
-
* one.
|
|
88
|
-
*
|
|
89
|
-
* Hover and focus stop the same timer, and the two are genuinely independent:
|
|
90
|
-
* a pointer can leave while focus stays, and focus can leave while the
|
|
91
|
-
* pointer stays. Uncounted, a reader who tabs into a message and then moves
|
|
92
|
-
* the pointer off it has the clock restarted underneath them, which is the
|
|
93
|
-
* case the pause exists for. Only the last one out starts it again.
|
|
94
|
-
*/
|
|
95
16
|
this.holds = 0;
|
|
96
17
|
this.handlePause = () => {
|
|
97
18
|
this.holds += 1;
|
|
@@ -104,19 +25,6 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
104
25
|
}
|
|
105
26
|
};
|
|
106
27
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Whether the message is still showing.
|
|
109
|
-
*
|
|
110
|
-
* The item always applies: it goes when it is asked to, having announced
|
|
111
|
-
* `helix-dismiss` first, and a consumer who owns the list refuses that event
|
|
112
|
-
* to take the decision instead.
|
|
113
|
-
*
|
|
114
|
-
* **No `default-open`, unlike the four mixins.** A `default-` seed exists so
|
|
115
|
-
* markup can choose a starting state without claiming ownership, and there is
|
|
116
|
-
* only one starting state that makes sense: a message that has been rendered
|
|
117
|
-
* is showing. An attribute whose only legal value is the default is an
|
|
118
|
-
* attribute nobody can use.
|
|
119
|
-
*/
|
|
120
28
|
get open() {
|
|
121
29
|
return this.openState ?? true;
|
|
122
30
|
}
|
|
@@ -125,21 +33,10 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
125
33
|
this.openState = next;
|
|
126
34
|
this.requestUpdate('open', previous);
|
|
127
35
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Asks to be taken away, and reports whether the request stood.
|
|
130
|
-
*
|
|
131
|
-
* Announced before anything happens, so `preventDefault()` is a refusal and
|
|
132
|
-
* the message stays. The event still carries the label, because a consumer
|
|
133
|
-
* removing this from their own state has to know which one went, and that is
|
|
134
|
-
* true whether or not the item hid itself first.
|
|
135
|
-
*/
|
|
136
36
|
dismiss() {
|
|
137
37
|
if (!this.open) {
|
|
138
38
|
return true;
|
|
139
39
|
}
|
|
140
|
-
/* The clock stops either way. A timer left running on a message that has
|
|
141
|
-
gone would fire into nothing, and would hold the item alive until it
|
|
142
|
-
did. */
|
|
143
40
|
clearTimeout(this.timer);
|
|
144
41
|
this.timer = undefined;
|
|
145
42
|
return requestChange({
|
|
@@ -148,18 +45,11 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
148
45
|
this.openState = false;
|
|
149
46
|
this.requestUpdate('open', previous);
|
|
150
47
|
},
|
|
151
|
-
/* `helix-dismiss` answers a controlled `open`, which reads oddly and is
|
|
152
|
-
exact. A toast is not an overlay that opens and closes: `next` below is
|
|
153
|
-
`false` and nothing here ever calls `requestOpen(true)`, so it only
|
|
154
|
-
ever asks in one direction, and the verb for that direction is
|
|
155
|
-
`dismiss`. A consumer controlling `open` therefore answers
|
|
156
|
-
`onhelix-dismiss`, and the generated types ask for exactly that. */
|
|
157
48
|
event: () => this.dismissEvent(),
|
|
158
49
|
host: this,
|
|
159
50
|
next: false,
|
|
160
51
|
});
|
|
161
52
|
}
|
|
162
|
-
/** Stops the clock, leaving whatever is left of it. */
|
|
163
53
|
pause() {
|
|
164
54
|
if (this.timer === undefined || this.startedAt === undefined) {
|
|
165
55
|
return;
|
|
@@ -168,13 +58,8 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
168
58
|
this.timer = undefined;
|
|
169
59
|
this.remaining = Math.max((this.remaining ?? 0) - (Date.now() - this.startedAt), 0);
|
|
170
60
|
this.startedAt = undefined;
|
|
171
|
-
/* Said on the host so the indicator can freeze with the clock. A `data-`
|
|
172
|
-
attribute rather than a property, because it is written after upgrade
|
|
173
|
-
from a pointer moving: nothing declares it and no server rendered it,
|
|
174
|
-
which is the same reason `Carousel` names its slides from here. */
|
|
175
61
|
this.toggleAttribute('data-paused', true);
|
|
176
62
|
}
|
|
177
|
-
/** Starts it again from where it stopped. */
|
|
178
63
|
resume() {
|
|
179
64
|
if (this.timer !== undefined || this.remaining === undefined || this.remaining <= 0) {
|
|
180
65
|
return;
|
|
@@ -188,56 +73,21 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
188
73
|
}
|
|
189
74
|
disconnectedCallback() {
|
|
190
75
|
super.disconnectedCallback();
|
|
191
|
-
// A message removed while its timer is running would otherwise fire into
|
|
192
|
-
// nothing, and hold the item alive until it did.
|
|
193
76
|
clearTimeout(this.timer);
|
|
194
77
|
this.timer = undefined;
|
|
195
78
|
}
|
|
196
79
|
willUpdate(changed) {
|
|
197
80
|
super.willUpdate(changed);
|
|
198
|
-
/* Mirrored for CSS rather than reflected onto `open` itself, which is the
|
|
199
|
-
library's rule about state: a reflected `open` is indistinguishable from
|
|
200
|
-
one the consumer declared, and that attribute is what ownership is read
|
|
201
|
-
from. `data-closed` can never be mistaken for a declaration. */
|
|
202
|
-
/* **`data-closed`, because open is the resting state.** A toast opens by
|
|
203
|
-
default, so a `data-open` would appear on a consumer's server-rendered
|
|
204
|
-
`<helix-toast-item>` without React having sent it. Inverting keeps the
|
|
205
|
-
`data-` hook for the headless consumer this publishes it for, and moves
|
|
206
|
-
the write to the side that only happens after somebody dismisses the
|
|
207
|
-
message, which is long past hydration. */
|
|
208
81
|
this.toggleAttribute('data-closed', !this.open);
|
|
209
|
-
/* And `hidden` beside it, because `data-closed` is a hook and not a
|
|
210
|
-
mechanism. With the hiding living only in the styled sheet's
|
|
211
|
-
`opacity: 0` and `visibility: hidden`, a consumer on this tier has a
|
|
212
|
-
dismissed message that never goes away: measured, a closed base toast
|
|
213
|
-
printed its text on the page.
|
|
214
|
-
It costs the styled tier nothing, the same as the accordion: `:host` sets
|
|
215
|
-
`display: grid` unconditionally so the author rule beats the UA sheet,
|
|
216
|
-
and the exit still animates its row down from `1fr`. */
|
|
217
82
|
this.toggleAttribute('hidden', !this.open);
|
|
218
83
|
}
|
|
219
84
|
firstUpdated(changed) {
|
|
220
85
|
super.firstUpdated(changed);
|
|
221
|
-
/* Here rather than in `connectedCallback`, which the server never calls, and
|
|
222
|
-
started only once: the clock belongs to this message's life on screen. */
|
|
223
86
|
if (this.duration !== undefined && this.duration > 0) {
|
|
224
87
|
this.remaining = this.duration;
|
|
225
88
|
this.resume();
|
|
226
89
|
}
|
|
227
90
|
}
|
|
228
|
-
/**
|
|
229
|
-
* The mark for the message's tone, behind a seam.
|
|
230
|
-
*
|
|
231
|
-
* Slot fallback, so a consumer replaces it by slotting; and a method, so the
|
|
232
|
-
* tier above replaces it by overriding. Those answer different people: the
|
|
233
|
-
* slot is for the page and the method is for whoever is building the styled
|
|
234
|
-
* layer, and a fallback with only the first is one a tier can restyle and
|
|
235
|
-
* cannot change.
|
|
236
|
-
*
|
|
237
|
-
* The mark is passed in rather than looked up again, since `render` has
|
|
238
|
-
* already resolved the tone and a second lookup is a second place for the
|
|
239
|
-
* fallback to disagree with the tone around it.
|
|
240
|
-
*/
|
|
241
91
|
renderIcon(mark) {
|
|
242
92
|
return html `<slot name="icon">
|
|
243
93
|
<svg part="icon-mark"
|
|
@@ -250,15 +100,6 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
250
100
|
</svg>
|
|
251
101
|
</slot>`;
|
|
252
102
|
}
|
|
253
|
-
/**
|
|
254
|
-
* The mark on the dismiss control, behind a seam.
|
|
255
|
-
*
|
|
256
|
-
* Slot fallback, so a consumer replaces it by slotting; and a method, so the
|
|
257
|
-
* tier above replaces it by overriding. Those answer different people: the
|
|
258
|
-
* slot is for the page and the method is for whoever is building the styled
|
|
259
|
-
* layer, and a fallback with only the first is one a tier can restyle and
|
|
260
|
-
* cannot change.
|
|
261
|
-
*/
|
|
262
103
|
renderDismissIcon() {
|
|
263
104
|
return html `<slot name="dismiss-icon">
|
|
264
105
|
<svg part="dismiss-mark" viewBox=${x.viewBox} fill="currentColor" aria-hidden="true">
|
|
@@ -267,12 +108,6 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
|
|
|
267
108
|
</slot>`;
|
|
268
109
|
}
|
|
269
110
|
render() {
|
|
270
|
-
/* Nothing when no tone was given, which is the common message: "Project
|
|
271
|
-
saved" is not an error, a warning or an announcement, and an icon there
|
|
272
|
-
would be decoration standing where meaning is supposed to be. `Callout`
|
|
273
|
-
falls back to `info` because a callout exists to set a passage apart and
|
|
274
|
-
is always *about* something; a toast is just the next thing that
|
|
275
|
-
happened. */
|
|
276
111
|
const mark = toneMark(this.tone);
|
|
277
112
|
return html `<div
|
|
278
113
|
part="item"
|
|
@@ -7,67 +7,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary The region messages arrive in, and the only thing that announces them.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* **The region is why this is one family rather than two.** A live region has
|
|
15
|
-
* to be in the document *before* a message is put into it: assistive technology
|
|
16
|
-
* subscribes to the region, and a region that arrives already holding its
|
|
17
|
-
* message is frequently not announced at all. So the container is a part of the
|
|
18
|
-
* family and not an arranger of complete things, which is the same reasoning
|
|
19
|
-
* that puts `Radio.Group` inside its own family.
|
|
20
|
-
*
|
|
21
|
-
* **Polite, never assertive.** `role="alert"` interrupts whatever a reader is
|
|
22
|
-
* being told, and almost nothing that arrives in a corner of the screen has
|
|
23
|
-
* earned that. Something that genuinely cannot wait is a `Dialog`, which stops
|
|
24
|
-
* the page for everyone rather than only for the people who cannot see it.
|
|
25
|
-
*
|
|
26
|
-
* **It never creates or destroys anything.** The consumer renders items and
|
|
27
|
-
* removes them; asking to go is a cancelable `helix-dismiss` from the item. An
|
|
28
|
-
* item that has gone hides itself and stays in the tree, so a consumer whose
|
|
29
|
-
* state still lists it does not have their framework fighting a node this
|
|
30
|
-
* component took away. It hides without waiting to be told, unless the consumer
|
|
31
|
-
* declared `open` and said the state is theirs.
|
|
32
|
-
* There is no `toast.show()`, because an imperative queue inside a component is
|
|
33
|
-
* a second copy of the application's state that nothing can render from: an app
|
|
34
|
-
* that wants a `toast()` function writes a thin one over the state it already
|
|
35
|
-
* has, and keeps the ability to render the same messages on the server, in a
|
|
36
|
-
* test, or twice.
|
|
37
|
-
*
|
|
38
|
-
* @since 0.1.0
|
|
39
|
-
* @status experimental
|
|
40
|
-
* @category overlays
|
|
41
|
-
* @aliases Toaster, Snackbar, Notifications
|
|
42
|
-
* @documentation https://helix.fusion.dev/components/toast
|
|
43
|
-
*
|
|
44
|
-
* @slot - The messages, as `helix-toast-item` elements.
|
|
45
|
-
*
|
|
46
|
-
* @csspart region - The stack the messages are in.
|
|
47
|
-
*/
|
|
48
10
|
let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
|
|
49
11
|
constructor() {
|
|
50
12
|
super(...arguments);
|
|
51
|
-
/**
|
|
52
|
-
* Accessible name for the region, such as "Notifications".
|
|
53
|
-
*/
|
|
54
13
|
this.label = 'Notifications';
|
|
55
14
|
}
|
|
56
15
|
willUpdate(changed) {
|
|
57
16
|
super.willUpdate(changed);
|
|
58
|
-
/* On the host through internals, so the region is a landmark to anything
|
|
59
|
-
walking the light DOM as well as to a screen reader, and so nothing is
|
|
60
|
-
written onto the host as an attribute a server's markup never had. */
|
|
61
17
|
hostAria(this.internals, { label: this.label, role: 'status' });
|
|
62
18
|
this.internals.ariaLive = 'polite';
|
|
63
|
-
/* Additions are what a reader needs; a message going away is not news, and
|
|
64
|
-
announcing removals means every dismissal is read out. */
|
|
65
19
|
this.internals.ariaRelevant = 'additions';
|
|
66
|
-
/* `role="status"` carries an implicit `aria-atomic="true"`, which means the
|
|
67
|
-
whole region is re-read whenever any part of it changes. With a stack of
|
|
68
|
-
four, the fifth arrival announces all five. `additions` alone does not
|
|
69
|
-
help: it says which mutations count, not how much is read out when one
|
|
70
|
-
does. Both are needed, and only one was here. */
|
|
71
20
|
this.internals.ariaAtomic = 'false';
|
|
72
21
|
}
|
|
73
22
|
render() {
|
|
@@ -8,80 +8,15 @@ import { customElement, DisabledCascadeController, HelixFormElement, hostAria, I
|
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { HelixHeadlessToggle } from '../root/toggle-root.js';
|
|
11
|
-
/**
|
|
12
|
-
* @summary A row of options with the selection marked by a moving indicator.
|
|
13
|
-
*
|
|
14
|
-
* @description
|
|
15
|
-
* The indicator is one element that travels, not a background that appears on
|
|
16
|
-
* whichever item is current. That difference is the whole point: a fill that
|
|
17
|
-
* swaps is two things changing at once and reads as a flicker, while one that
|
|
18
|
-
* moves is a single object the eye follows, and it says where the selection
|
|
19
|
-
* came from as well as where it went.
|
|
20
|
-
*
|
|
21
|
-
* Its whole box is measured from the selected item and written onto
|
|
22
|
-
* the indicator as custom properties, because labels are different lengths and
|
|
23
|
-
* no proportion of the group is the right answer. Measurement is client-side by
|
|
24
|
-
* necessity, so the server renders the indicator hidden and it fades in once it
|
|
25
|
-
* has somewhere to be, which is the harmless direction: it appears rather than
|
|
26
|
-
* jumping from a wrong place.
|
|
27
|
-
*
|
|
28
|
-
* A radio group, and only that. Navigation is links marked with `aria-current`
|
|
29
|
-
* and styled by the `data-navigation-item` recipe: radio semantics would announce a
|
|
30
|
-
* set of destinations as a set of choices. A tab controls a panel.
|
|
31
|
-
*
|
|
32
|
-
* Being a radio group, it is a form control, and it says so through
|
|
33
|
-
* `HelixFormElement`: it reports its value under a `name`, comes back to the
|
|
34
|
-
* markup's choice on reset, and restores the one a reader made when the browser
|
|
35
|
-
* brings a form back.
|
|
36
|
-
*
|
|
37
|
-
* The semantics were already right: the host has carried `role="radiogroup"`
|
|
38
|
-
* and the items `role="radio"` from the start. What none of it did was reach a
|
|
39
|
-
* form. A button in a shadow root submits nothing whatever its role, so the
|
|
40
|
-
* value has to be reported by the host, which is the only thing a form can
|
|
41
|
-
* see.
|
|
42
|
-
*
|
|
43
|
-
* @since 0.1.0
|
|
44
|
-
* @status experimental
|
|
45
|
-
* @aliases SegmentedControl, Segmented, Navigation, Switcher
|
|
46
|
-
* @documentation https://helix.fusion.dev/components/toggle-group
|
|
47
|
-
*
|
|
48
|
-
* @slot - The group's items.
|
|
49
|
-
*
|
|
50
|
-
* @event {Event} change - Announced once the value has moved.
|
|
51
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired before the answer moves, and cancelable. The only way to refuse one.
|
|
52
|
-
* @event {HelixSelectEvent} helix-select - Fired when an item is chosen. Dispatched by the item and listened for here. Cancelable: refusing it takes the decision and nothing moves.
|
|
53
|
-
* @event {Event} input - Announced once the value has moved.
|
|
54
|
-
*
|
|
55
|
-
* @csspart divider - The rule after an inline label, separating it from the items.
|
|
56
|
-
* @csspart feedback - What is wrong with it.
|
|
57
|
-
* @csspart hint - Help under the row, while nothing is wrong.
|
|
58
|
-
* @csspart indicator - The moving marker behind the selected item.
|
|
59
|
-
* @csspart label - The question, over the row.
|
|
60
|
-
* @csspart row - The line the track sits on, with whatever flanks it.
|
|
61
|
-
* @csspart track - The bar the items sit in.
|
|
62
|
-
*/
|
|
63
11
|
let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinControllerValue(MixinField(HelixFormElement)) {
|
|
64
12
|
constructor() {
|
|
65
13
|
super(...arguments);
|
|
66
14
|
this.indicator = new IndicatorController(this, {
|
|
67
|
-
// The same exact match `sync` makes: nothing chosen means nothing marked,
|
|
68
|
-
// and the controller takes the mark away rather than parking it on item one.
|
|
69
15
|
active: () => this.items.find((item) => item.value === this.value),
|
|
70
16
|
indicator: () => this.renderRoot?.querySelector('[part="indicator"]'),
|
|
71
17
|
surface: () => this.renderRoot?.querySelector('[part="track"]'),
|
|
72
18
|
});
|
|
73
|
-
/* Moving and choosing are one act, which is the radio group pattern this
|
|
74
|
-
control also follows, and is what the controller exists to stop being
|
|
75
|
-
written per component. */
|
|
76
|
-
/* Registered rather than held. A controller adds itself to the host, so there
|
|
77
|
-
is no reference to keep: nothing here talks to it after construction, which
|
|
78
|
-
is why it is `protected` and not `private`. A private field nothing reads
|
|
79
|
-
is an unused one, and the honest reading is that the subclass tier is where
|
|
80
|
-
a consumer would reach for it. */
|
|
81
19
|
this.keys = new KeyboardNavigationController(this, { items: () => this.items });
|
|
82
|
-
/* Stated once, and the group never thinks about it again. The controller is
|
|
83
|
-
what stops the cascade being hand-called from whichever methods a component
|
|
84
|
-
happens to have. */
|
|
85
20
|
this.cascade = new DisabledCascadeController(this, {
|
|
86
21
|
items: () => this.items,
|
|
87
22
|
});
|
|
@@ -89,22 +24,8 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
89
24
|
this.requestValue(event.detail.value);
|
|
90
25
|
};
|
|
91
26
|
}
|
|
92
|
-
/** Marks the chosen item, separately from placing the indicator. */
|
|
93
27
|
sync() {
|
|
94
|
-
/* An exact match and nothing else, never `resolveActiveItem`, which falls
|
|
95
|
-
* back to the first enabled item: a group with no value would mark one
|
|
96
|
-
* anyway, `aria-checked="true"` and the thumb under it and full selected
|
|
97
|
-
* styling, while the group reported nothing. A `required` group would then
|
|
98
|
-
* tell a reader a value is missing while they are looking at one.
|
|
99
|
-
*
|
|
100
|
-
* That fallback is right where it came from. A tab set always shows a panel,
|
|
101
|
-
* so something has to be current; a group that reports a form value has the
|
|
102
|
-
* honest option of reporting none. */
|
|
103
28
|
const active = this.items.find((item) => item.value === this.value);
|
|
104
|
-
/* With nothing chosen there is no selection to carry the tab stop, so the
|
|
105
|
-
first item holds one until there is. Otherwise an unanswered group is a
|
|
106
|
-
control the keyboard cannot enter, which is what the fallback was quietly
|
|
107
|
-
also doing for it. */
|
|
108
29
|
const entry = active ? undefined : this.items.find((item) => !item.disabled);
|
|
109
30
|
for (const item of this.items) {
|
|
110
31
|
item.selected = item === active;
|
|
@@ -114,15 +35,6 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
114
35
|
get items() {
|
|
115
36
|
return [...this.children].filter((child) => child instanceof HelixHeadlessToggle);
|
|
116
37
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Watches the group and every item it currently holds.
|
|
119
|
-
*
|
|
120
|
-
* Called again on `slotchange`, because the set was read once at first paint:
|
|
121
|
-
* an item added later was measured by nothing, so a group whose options
|
|
122
|
-
* arrive with the data had a thumb that stopped following the moment one of
|
|
123
|
-
* them changed width. Observing the same element twice is a no-op, so there
|
|
124
|
-
* is nothing to unwind first.
|
|
125
|
-
*/
|
|
126
38
|
watch() {
|
|
127
39
|
this.observer?.observe(this);
|
|
128
40
|
for (const item of this.items) {
|
|
@@ -132,14 +44,6 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
132
44
|
connectedCallback() {
|
|
133
45
|
super.connectedCallback();
|
|
134
46
|
this.addEventListener('helix-select', this.handleSelection);
|
|
135
|
-
/* The observer is rebuilt here, because nothing else does it.
|
|
136
|
-
*
|
|
137
|
-
* `firstUpdated` runs once for the life of the element and
|
|
138
|
-
* `disconnectedCallback` runs every time it moves, so a framework
|
|
139
|
-
* reordering a list left this permanently dead: the observer gone, nothing
|
|
140
|
-
* to rebuild it, and the component still rendering as though it were
|
|
141
|
-
* watching. Guarded on `hasUpdated`, because the first connection happens
|
|
142
|
-
* before the first render and there is nothing to watch yet. */
|
|
143
47
|
if (this.hasUpdated) {
|
|
144
48
|
this.measure();
|
|
145
49
|
this.watch();
|
|
@@ -154,85 +58,31 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
|
|
|
154
58
|
super.firstUpdated(changed);
|
|
155
59
|
this.sync();
|
|
156
60
|
this.indicator.place();
|
|
157
|
-
/* Per item, not only the host, which is what the controller's own observer
|
|
158
|
-
covers. An item can change width inside a track that keeps its size, and
|
|
159
|
-
the host never fires for that. */
|
|
160
61
|
this.measure();
|
|
161
62
|
this.watch();
|
|
162
63
|
}
|
|
163
|
-
/** The observer the items are registered on, rebuilt with them. */
|
|
164
64
|
measure() {
|
|
165
65
|
this.observer?.disconnect();
|
|
166
66
|
this.observer = new ResizeObserver(() => this.indicator.place());
|
|
167
67
|
}
|
|
168
68
|
updated(changed) {
|
|
169
69
|
super.updated(changed);
|
|
170
|
-
// Every update, not only the ones that name `value`: see the note on
|
|
171
|
-
// `ColorInput.updated`. The placement stays gated, because the indicator
|
|
172
|
-
// controller already places on every update of its own host.
|
|
173
70
|
this.sync();
|
|
174
71
|
if (changed.has('value')) {
|
|
175
72
|
this.indicator.place();
|
|
176
73
|
}
|
|
177
74
|
this.validate();
|
|
178
|
-
/* Every update, because the label and the message come and go with the
|
|
179
|
-
properties behind them, and an element reference to a node that has been
|
|
180
|
-
re-rendered is a reference to nothing. */
|
|
181
75
|
this.linkHost();
|
|
182
76
|
}
|
|
183
77
|
willUpdate(changed) {
|
|
184
78
|
super.willUpdate(changed);
|
|
185
|
-
// Through internals, so nothing lands on the host as an attribute. A nav
|
|
186
|
-
// renders a real `<nav>` in the shadow root and takes its semantics from
|
|
187
|
-
// that, so the host stays neutral.
|
|
188
|
-
/* The label goes on as a string as well as the element reference
|
|
189
|
-
`linkHost` sets, because this is the half a server can write. The
|
|
190
|
-
reference wins on the client, which is what lets a consumer slot a label
|
|
191
|
-
richer than a string. */
|
|
192
79
|
hostAria(this.internals, { label: this.label, role: 'radiogroup' });
|
|
193
|
-
// `null` rather than an empty string when nothing is chosen, which is the
|
|
194
|
-
// difference between a control that submits nothing and one that submits a
|
|
195
|
-
// blank.
|
|
196
80
|
this.internals.setFormValue(this.value === '' ? null : this.value);
|
|
197
81
|
}
|
|
198
|
-
/**
|
|
199
|
-
* The verdict on the answer `required` asks for.
|
|
200
|
-
*
|
|
201
|
-
* `required` arrives with `MixinField` and enforces nothing on its own: the
|
|
202
|
-
* attribute reflects and the marker renders beside the label, and without this
|
|
203
|
-
* a form with an unanswered group submits anyway. The marker is a promise to
|
|
204
|
-
* the reader that something will stop them.
|
|
205
|
-
*
|
|
206
|
-
* The anchor is the track rather than the host, so the browser's own bubble
|
|
207
|
-
* points at the control rather than at a box with nothing in it.
|
|
208
|
-
*/
|
|
209
82
|
validate() {
|
|
210
83
|
this.reportRequired(this.value === '', 'Choose one to continue.', 'track');
|
|
211
84
|
}
|
|
212
|
-
/**
|
|
213
|
-
* The row the track sits in, so something can sit beside it.
|
|
214
|
-
*
|
|
215
|
-
* **Bare slots, and a wrapper only around the row itself.** An unassigned
|
|
216
|
-
* slot is `display: contents` in the UA stylesheet and generates no box at
|
|
217
|
-
* all, so a group with nothing slotted lays out exactly as it did before this
|
|
218
|
-
* existed: no grid track, no flex item, no gap. Wrapping each side in a div
|
|
219
|
-
* instead would paint an empty inset band on every group in the library that
|
|
220
|
-
* never asked for one, which is the measurement Card already paid for.
|
|
221
|
-
*
|
|
222
|
-
* The row does always generate a box, and is entitled to: the track is always
|
|
223
|
-
* in it.
|
|
224
|
-
*/
|
|
225
85
|
render() {
|
|
226
|
-
/* The track is an element rather than the host, and has to be: the label,
|
|
227
|
-
the hint and the message are the host's children too, so a track on the
|
|
228
|
-
host makes them flex items in the same row as the segments. The track also
|
|
229
|
-
owns the box the indicator is measured against, which is why `place` reads
|
|
230
|
-
it rather than `this`. */
|
|
231
|
-
/* **Inside the track when it is placed at the start, not beside it.** A
|
|
232
|
-
label sitting outside reads as a field's label that happens to be on one
|
|
233
|
-
line; inside, with a rule after it, the group reads as one control that
|
|
234
|
-
says what it is for. The divider is what makes that legible: without it
|
|
235
|
-
the word is a first segment that cannot be pressed. */
|
|
236
86
|
const inside = this.labelPlacement === 'start';
|
|
237
87
|
const label = this.renderGroupLabel();
|
|
238
88
|
return html `${inside ? nothing : label}
|
|
@@ -7,108 +7,30 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, HelixSelectEvent } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary One option in a toggle group.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* Renders a `<button role="radio">`. This is a form control and nothing else.
|
|
15
|
-
*
|
|
16
|
-
* Navigation is not this. It is links marked with `aria-current` and styled by
|
|
17
|
-
* the `data-navigation-item` recipe in the base layer, which carries the same look
|
|
18
|
-
* without radio semantics announcing destinations as choices.
|
|
19
|
-
*
|
|
20
|
-
* @since 0.1.0
|
|
21
|
-
* @status experimental
|
|
22
|
-
* @category actions
|
|
23
|
-
* @aliases Segment, NavItem, Option
|
|
24
|
-
* @documentation https://helix.fusion.dev/components/toggle
|
|
25
|
-
*
|
|
26
|
-
* @slot - The item's label.
|
|
27
|
-
*
|
|
28
|
-
* @event {HelixSelectEvent} helix-select - Fired when the item is chosen. Cancelable: refusing it takes the decision and nothing moves.
|
|
29
|
-
*
|
|
30
|
-
* @csspart item - The radio button.
|
|
31
|
-
*/
|
|
32
10
|
let HelixHeadlessToggle = class HelixHeadlessToggle extends HelixElement {
|
|
33
11
|
constructor() {
|
|
34
12
|
super(...arguments);
|
|
35
|
-
/**
|
|
36
|
-
* Whether the item can be chosen.
|
|
37
|
-
*/
|
|
38
13
|
this.disabled = false;
|
|
39
|
-
/** What the markup said, read once at construction for the reason above. */
|
|
40
14
|
this.seeded = this.hasAttribute('selected');
|
|
41
|
-
/**
|
|
42
|
-
* Whether this item holds the group's tab stop while nothing is chosen.
|
|
43
|
-
*
|
|
44
|
-
* The stop normally rides on the selection, and an unanswered group has no
|
|
45
|
-
* selection, so without this the keyboard cannot reach it. Written by the
|
|
46
|
-
* group, with no attribute: it is coordination between elements, and
|
|
47
|
-
* `tabindex` on a host the consumer rendered is the change a React consumer's
|
|
48
|
-
* hydration reports and does not patch up.
|
|
49
|
-
*/
|
|
50
15
|
this.focusable = false;
|
|
51
|
-
/**
|
|
52
|
-
* Whether this item is the current one.
|
|
53
|
-
*/
|
|
54
16
|
this.selected = false;
|
|
55
|
-
/**
|
|
56
|
-
* Identifies the item to the group.
|
|
57
|
-
*/
|
|
58
17
|
this.value = '';
|
|
59
18
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Whether the markup declared this one chosen, which is what a reset returns to.
|
|
62
|
-
*
|
|
63
|
-
* `<option selected>` reflects to `defaultSelected` and `option.selected` is
|
|
64
|
-
* live state that does not reflect, and those are two different facts. This
|
|
65
|
-
* element has one `selected` doing both jobs, so what is captured here is what
|
|
66
|
-
* the markup said at upgrade, before anything could overwrite it: read the
|
|
67
|
-
* attribute later and the listbox's own writes answer instead.
|
|
68
|
-
*
|
|
69
|
-
* **The deeper half is not this.** State reflecting under the declaration's
|
|
70
|
-
* name is the fault `TreeItem` avoids by mirroring to `data-selected`.
|
|
71
|
-
* Splitting them means moving what every stylesheet selects on, which is a
|
|
72
|
-
* change of its own rather than a property being added.
|
|
73
|
-
*/
|
|
74
19
|
get defaultSelected() {
|
|
75
20
|
return this.seeded;
|
|
76
21
|
}
|
|
77
22
|
set defaultSelected(next) {
|
|
78
23
|
this.seeded = next;
|
|
79
24
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Moves focus to this item's own control.
|
|
82
|
-
*
|
|
83
|
-
* The group drives arrow-key navigation and cannot reach the control itself,
|
|
84
|
-
* which is inside this shadow root. Exposing the intent as a method keeps the
|
|
85
|
-
* group out of the child's internals.
|
|
86
|
-
*/
|
|
87
25
|
focusItem() {
|
|
88
26
|
this.renderRoot.querySelector('[part="item"]')?.focus();
|
|
89
27
|
}
|
|
90
|
-
/** Chooses this item and announces it. */
|
|
91
28
|
select() {
|
|
92
29
|
if (this.disabled) {
|
|
93
30
|
return;
|
|
94
31
|
}
|
|
95
32
|
this.dispatchEvent(new HelixSelectEvent({ value: this.value }));
|
|
96
33
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Mirrors the chosen state where a stylesheet can select on it.
|
|
99
|
-
*
|
|
100
|
-
* **A custom state rather than the `selected` attribute.** The parent writes
|
|
101
|
-
* this: a listbox choosing a row, a tab strip moving the tab, a radio group
|
|
102
|
-
* answering for its set. Reflected, that puts the element's own bookkeeping
|
|
103
|
-
* onto markup a consumer wrote and a consumer's server never sent, which
|
|
104
|
-
* React reconciles and reports as a mismatch, and which a serialized element
|
|
105
|
-
* pasted back declares as a choice nobody made. `ElementInternals` holds it
|
|
106
|
-
* instead, where it is not in the DOM at all.
|
|
107
|
-
*
|
|
108
|
-
* Reading `selected` is unaffected and so is writing it: the attribute still
|
|
109
|
-
* seeds the property, which is what `<helix-toggle selected>` in a consumer's
|
|
110
|
-
* markup means. There is no write back.
|
|
111
|
-
*/
|
|
112
34
|
willUpdate(changed) {
|
|
113
35
|
super.willUpdate(changed);
|
|
114
36
|
this.toggleState('selected', this.selected);
|