@fusion.dev/helix-headless 0.153.0 → 0.155.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 +1885 -140
- package/dist/accordion/item/accordion-item.d.ts +15 -0
- package/dist/accordion/item/accordion-item.js +12 -1
- package/dist/accordion/root/accordion-root.d.ts +2 -1
- package/dist/accordion/root/accordion-root.js +1 -0
- package/dist/badge/root/badge-root.d.ts +15 -2
- package/dist/badge/root/badge-root.js +20 -3
- package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
- package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
- package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
- package/dist/button-group/root/button-group-root.d.ts +2 -0
- package/dist/button-group/root/button-group-root.js +1 -0
- package/dist/card/root/card-root.d.ts +31 -11
- package/dist/card/root/card-root.js +25 -11
- package/dist/code/root/code-root.d.ts +7 -0
- package/dist/code/root/code-root.js +1 -1
- package/dist/code-block/root/code-block-root.d.ts +37 -1
- package/dist/code-block/root/code-block-root.js +38 -5
- package/dist/color-input/root/color-input-root.d.ts +9 -3
- package/dist/color-input/root/color-input-root.js +3 -1
- package/dist/color-picker/channels.d.ts +5 -3
- package/dist/color-picker/channels.js +79 -21
- package/dist/color-picker/root/color-picker-root.d.ts +58 -5
- package/dist/color-picker/root/color-picker-root.js +75 -13
- package/dist/date-input/root/date-input-root.d.ts +1 -0
- package/dist/date-input/root/date-input-root.js +2 -1
- package/dist/date-picker/root/date-picker-root.d.ts +43 -13
- package/dist/date-picker/root/date-picker-root.js +54 -31
- package/dist/element-events.generated.js +2 -1
- package/dist/empty/root/empty-root.d.ts +19 -1
- package/dist/empty/root/empty-root.js +13 -4
- package/dist/icon/fetch.d.ts +20 -0
- package/dist/icon/fetch.js +70 -0
- package/dist/icon/root/icon-root.d.ts +27 -5
- package/dist/icon/root/icon-root.js +41 -17
- package/dist/navigation/root/navigation-root.d.ts +26 -0
- package/dist/navigation/root/navigation-root.js +58 -1
- package/dist/quote/root/quote-root.d.ts +17 -1
- package/dist/quote/root/quote-root.js +15 -2
- package/dist/select/root/select-root.d.ts +29 -3
- package/dist/select/root/select-root.js +34 -9
- package/dist/skeleton/root/skeleton-root.d.ts +7 -0
- package/dist/skeleton/root/skeleton-root.js +4 -1
- package/dist/slider/root/slider-root.d.ts +71 -9
- package/dist/slider/root/slider-root.js +129 -42
- package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
- package/dist/speed-dial/item/speed-dial-item.js +48 -7
- package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
- package/dist/speed-dial/root/speed-dial-root.js +50 -5
- package/dist/splitter/root/splitter-root.d.ts +22 -0
- package/dist/splitter/root/splitter-root.js +36 -7
- package/dist/stat/group/stat-group.d.ts +23 -4
- package/dist/stat/group/stat-group.js +13 -1
- package/dist/stat/root/stat-root.d.ts +46 -1
- package/dist/stat/root/stat-root.js +29 -5
- package/dist/steps/reach.d.ts +5 -1
- package/dist/steps/reach.js +2 -2
- package/dist/steps/root/steps-root.d.ts +37 -2
- package/dist/steps/root/steps-root.js +49 -7
- package/dist/steps/step/step.d.ts +6 -0
- package/dist/steps/step/step.js +4 -0
- package/dist/tabs/root/tabs-root.d.ts +37 -0
- package/dist/tabs/root/tabs-root.js +92 -2
- package/dist/tabs/tab/tab.d.ts +38 -2
- package/dist/tabs/tab/tab.js +45 -4
- package/dist/tag/group/tag-group.d.ts +29 -3
- package/dist/tag/group/tag-group.js +42 -11
- package/dist/toggle/root/toggle-root.d.ts +2 -0
- package/dist/toggle/root/toggle-root.js +1 -1
- package/dist/tree/item/tree-item.d.ts +10 -0
- package/dist/tree/item/tree-item.js +12 -0
- package/dist/tree/root/tree-root.d.ts +40 -1
- package/dist/tree/root/tree-root.js +120 -4
- package/dist/tree/selection.d.ts +6 -0
- package/dist/tree/selection.js +11 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +1412 -207
- package/types/vue.d.ts +294 -65
- package/types/words.d.ts +49 -1
|
@@ -4,9 +4,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, customElement, formatNumber, formatWords, HelixFormElement, MixinControllerValue, MixinField, restoreValue, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, customElement, formatNumber, formatWords, HelixFormElement, MixinControllerValue, MixinField, reportValues, restoreValue, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
+
const SLIDER_KEYS = new Set([
|
|
11
|
+
'ArrowDown',
|
|
12
|
+
'ArrowLeft',
|
|
13
|
+
'ArrowRight',
|
|
14
|
+
'ArrowUp',
|
|
15
|
+
'End',
|
|
16
|
+
'Home',
|
|
17
|
+
'PageDown',
|
|
18
|
+
'PageUp',
|
|
19
|
+
]);
|
|
10
20
|
let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue(MixinField(HelixFormElement)) {
|
|
11
21
|
constructor() {
|
|
12
22
|
super(...arguments);
|
|
@@ -24,21 +34,29 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
24
34
|
this.minStepsBetweenThumbs = 0;
|
|
25
35
|
this.range = false;
|
|
26
36
|
this.rangeText = '{start} to {end}';
|
|
37
|
+
this.readOnly = false;
|
|
27
38
|
this.showValue = false;
|
|
28
39
|
this.step = 1;
|
|
40
|
+
this.thumbLabel = 'thumb {index}';
|
|
29
41
|
this.tooltip = false;
|
|
30
42
|
this.handleInput = (event) => {
|
|
31
43
|
event.stopPropagation();
|
|
32
44
|
const control = event.target;
|
|
45
|
+
const at = Number(control.dataset.index ?? 0);
|
|
46
|
+
if (this.readOnly) {
|
|
47
|
+
restoreValue(control, this.range ? String(this.points[at]) : this.value);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
33
50
|
if (this.range) {
|
|
34
|
-
const
|
|
51
|
+
const points = this.points;
|
|
35
52
|
const moved = Number(control.value);
|
|
36
53
|
const gap = this.gap;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
const low = at > 0 ? (points[at - 1] ?? this.min) + gap : Number.NEGATIVE_INFINITY;
|
|
55
|
+
const high = at < points.length - 1 ? (points[at + 1] ?? this.max) - gap : Number.POSITIVE_INFINITY;
|
|
56
|
+
this.requestValue(points
|
|
57
|
+
.map((one, index) => (index === at ? Math.min(high, Math.max(low, moved)) : one))
|
|
58
|
+
.join(','));
|
|
59
|
+
restoreValue(control, String(this.points[at]));
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
44
62
|
const next = control.value;
|
|
@@ -47,16 +65,30 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
47
65
|
};
|
|
48
66
|
this.handleChange = (event) => {
|
|
49
67
|
event.stopPropagation();
|
|
50
|
-
this.
|
|
68
|
+
if (!this.readOnly) {
|
|
69
|
+
this.announceChange();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
this.handleKeydown = (event) => {
|
|
73
|
+
if (this.readOnly && SLIDER_KEYS.has(event.key)) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
}
|
|
51
76
|
};
|
|
52
77
|
this.valueText = '';
|
|
78
|
+
this.handleThumbSlot = () => {
|
|
79
|
+
this.requestUpdate();
|
|
80
|
+
};
|
|
53
81
|
}
|
|
54
82
|
static { this.words = 'helix-slider'; }
|
|
55
83
|
get reserve() {
|
|
56
84
|
const widest = Math.max(this.said(this.min).length, this.said(this.max).length);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
const count = this.points.length;
|
|
86
|
+
if (!this.range) {
|
|
87
|
+
return widest;
|
|
88
|
+
}
|
|
89
|
+
return count > 2
|
|
90
|
+
? widest * count + 2 * (count - 1) + 4
|
|
91
|
+
: widest * 2 + formatWords(this, 'rangeText', { end: '', start: '' }).length;
|
|
60
92
|
}
|
|
61
93
|
get precision() {
|
|
62
94
|
return String(this.step).split('.')[1]?.length ?? 0;
|
|
@@ -83,13 +115,15 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
83
115
|
delegatesFocus: true,
|
|
84
116
|
}; }
|
|
85
117
|
get bounds() {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
const points = this.points;
|
|
119
|
+
return [points[0] ?? this.min, points.at(-1) ?? this.max];
|
|
120
|
+
}
|
|
121
|
+
get points() {
|
|
122
|
+
const parts = this.value.split(',').map((part) => Number.parseFloat(part));
|
|
123
|
+
if (parts.length < 2 || parts.some((part) => !Number.isFinite(part))) {
|
|
90
124
|
return [this.min, this.max];
|
|
91
125
|
}
|
|
92
|
-
return
|
|
126
|
+
return parts.sort((left, right) => left - right);
|
|
93
127
|
}
|
|
94
128
|
along(at) {
|
|
95
129
|
const span = this.max - this.min;
|
|
@@ -99,7 +133,12 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
99
133
|
return Math.min(1, Math.max(0, (at - this.min) / span));
|
|
100
134
|
}
|
|
101
135
|
sync() {
|
|
102
|
-
this.
|
|
136
|
+
if (this.range && this.name) {
|
|
137
|
+
reportValues(this.internals, this.name, this.points.map(String));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.internals.setFormValue(this.value);
|
|
141
|
+
}
|
|
103
142
|
this.refreshFeedback();
|
|
104
143
|
}
|
|
105
144
|
get fraction() {
|
|
@@ -146,15 +185,14 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
146
185
|
}
|
|
147
186
|
sanitize(text) {
|
|
148
187
|
if (this.range) {
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
to === undefined ||
|
|
152
|
-
!Number.isFinite(from) ||
|
|
153
|
-
!Number.isFinite(to)) {
|
|
188
|
+
const values = text.split(',').map((part) => Number.parseFloat(part));
|
|
189
|
+
if (values.length < 2 || values.some((one) => !Number.isFinite(one))) {
|
|
154
190
|
return `${this.min},${this.max}`;
|
|
155
191
|
}
|
|
156
|
-
|
|
157
|
-
|
|
192
|
+
return values
|
|
193
|
+
.map((one) => this.settled(one))
|
|
194
|
+
.sort((a, b) => a - b)
|
|
195
|
+
.join(',');
|
|
158
196
|
}
|
|
159
197
|
const parsed = Number.parseFloat(text);
|
|
160
198
|
const at = Number.isFinite(parsed) ? parsed : this.min + (this.max - this.min) / 2;
|
|
@@ -188,7 +226,31 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
188
226
|
super.updated(changed);
|
|
189
227
|
this.sync();
|
|
190
228
|
}
|
|
191
|
-
|
|
229
|
+
firstUpdated(changed) {
|
|
230
|
+
super.firstUpdated(changed);
|
|
231
|
+
this.finishHydration();
|
|
232
|
+
}
|
|
233
|
+
get thumbContent() {
|
|
234
|
+
return this.knownChildren.some((child) => child.slot === 'thumb' || child.slot === 'thumb-end');
|
|
235
|
+
}
|
|
236
|
+
renderThumbs() {
|
|
237
|
+
const slot = (end) => end
|
|
238
|
+
? html `<slot name="thumb-end" @slotchange=${this.handleThumbSlot}></slot>`
|
|
239
|
+
: html `<slot name="thumb" @slotchange=${this.handleThumbSlot}></slot>`;
|
|
240
|
+
const box = (fraction, end, thumb) => html `<span part="thumb" data-thumb=${thumb ?? nothing} style="--slider-at: ${fraction}"
|
|
241
|
+
>${slot(end)}</span
|
|
242
|
+
>`;
|
|
243
|
+
return html `<div part="thumbs" aria-hidden="true" ?hidden=${!this.thumbContent}>
|
|
244
|
+
${this.range
|
|
245
|
+
? this.points.map((at, index, all) => index === 0
|
|
246
|
+
? box(this.along(at), false, 'start')
|
|
247
|
+
: index === all.length - 1
|
|
248
|
+
? box(this.along(at), true, 'end')
|
|
249
|
+
: html `<span part="thumb" data-thumb="middle" style="--slider-at: ${this.along(at)}"></span>`)
|
|
250
|
+
: box(this.fraction, false)}
|
|
251
|
+
</div>`;
|
|
252
|
+
}
|
|
253
|
+
renderTooltips() {
|
|
192
254
|
if (!this.tooltip) {
|
|
193
255
|
return nothing;
|
|
194
256
|
}
|
|
@@ -197,49 +259,60 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
197
259
|
>`;
|
|
198
260
|
return html `<div part="tooltips" aria-hidden="true">
|
|
199
261
|
${this.range
|
|
200
|
-
?
|
|
201
|
-
bubble(this.along(start), this.said(start), 'start'),
|
|
202
|
-
bubble(this.along(end), this.said(end), 'end'),
|
|
203
|
-
]
|
|
262
|
+
? this.points.map((at, index, all) => bubble(this.along(at), this.said(at), index === 0 ? 'start' : index === all.length - 1 ? 'end' : 'middle'))
|
|
204
263
|
: bubble(this.fraction, this.said(this.value))}
|
|
205
264
|
</div>`;
|
|
206
265
|
}
|
|
207
266
|
render() {
|
|
208
267
|
const [start, end] = this.bounds;
|
|
209
|
-
const
|
|
268
|
+
const ids = this.points.map((_, index, all) => index === 0 ? 'control' : index === all.length - 1 ? 'control-end' : `control-${index}`);
|
|
269
|
+
const points = this.points;
|
|
270
|
+
const last = points.length - 1;
|
|
271
|
+
const thumb = (at, index) => {
|
|
272
|
+
const which = index === 0 ? 'start' : index === last ? 'end' : 'middle';
|
|
273
|
+
const named = which === 'middle' ? `thumb-name-${index}` : `${which}-name`;
|
|
274
|
+
const before = points[index - 1];
|
|
275
|
+
const after = points[index + 1];
|
|
276
|
+
return html `<input
|
|
210
277
|
part="control"
|
|
211
|
-
id=${
|
|
278
|
+
id=${ids[index] ?? nothing}
|
|
212
279
|
data-thumb=${which}
|
|
280
|
+
data-index=${index}
|
|
213
281
|
type="range"
|
|
214
282
|
style="--slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}${this.axis}"
|
|
215
283
|
min=${this.min}
|
|
216
284
|
max=${this.max}
|
|
217
285
|
step=${this.step}
|
|
218
286
|
.value=${String(at)}
|
|
219
|
-
aria-labelledby=${this.labeled ? `${this.labelId} ${
|
|
220
|
-
aria-valuemin=${
|
|
221
|
-
aria-valuemax=${
|
|
287
|
+
aria-labelledby=${this.labeled ? `${this.labelId} ${named}` : named}
|
|
288
|
+
aria-valuemin=${before === undefined ? nothing : before + this.gap}
|
|
289
|
+
aria-valuemax=${after === undefined ? nothing : after - this.gap}
|
|
222
290
|
aria-valuetext=${this.spoken(at)}
|
|
223
291
|
aria-describedby=${this.describedBy ?? nothing}
|
|
224
292
|
aria-invalid=${this.invalidity ?? nothing}
|
|
293
|
+
aria-readonly=${this.readOnly ? 'true' : nothing}
|
|
225
294
|
?disabled=${this.isDisabled}
|
|
226
295
|
@input=${this.handleInput}
|
|
227
296
|
@change=${this.handleChange}
|
|
297
|
+
@keydown=${this.handleKeydown}
|
|
228
298
|
/>`;
|
|
299
|
+
};
|
|
229
300
|
return html `${this.renderLabel()}
|
|
230
301
|
${this.range
|
|
231
302
|
? html `<span id="start-name" hidden>${this.minimumLabel}</span
|
|
232
|
-
|
|
303
|
+
>${this.points.slice(1, -1).map((_, index) => html `<span id=${`thumb-name-${index + 1}`} hidden
|
|
304
|
+
>${formatWords(this, 'thumbLabel', { index: index + 2 })}</span
|
|
305
|
+
>`)}<span id="end-name" hidden>${this.maximumLabel}</span>`
|
|
233
306
|
: nothing}
|
|
234
307
|
<div
|
|
235
308
|
part="field"
|
|
236
309
|
data-markers=${this.markerValues.length > 0 ? '' : nothing}
|
|
237
310
|
data-range=${this.range ? '' : nothing}
|
|
311
|
+
data-thumbs=${this.thumbContent ? '' : nothing}
|
|
238
312
|
data-tooltip=${this.tooltip ? '' : nothing}
|
|
239
313
|
style="--slider-fraction: ${this.fraction}; --slider-from: ${this.along(start)}; --slider-to: ${this.along(end)}"
|
|
240
314
|
>
|
|
241
|
-
${this.range ? thumb(
|
|
242
|
-
${this.range ? thumb('end', end) : nothing}
|
|
315
|
+
${this.range ? this.points.map((at, index) => thumb(at, index)) : nothing}
|
|
243
316
|
${this.range
|
|
244
317
|
? nothing
|
|
245
318
|
: html `<input
|
|
@@ -256,9 +329,11 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
256
329
|
aria-valuetext=${this.spoken(this.value)}
|
|
257
330
|
aria-describedby=${this.describedBy ?? nothing}
|
|
258
331
|
aria-invalid=${this.invalidity ?? nothing}
|
|
332
|
+
aria-readonly=${this.readOnly ? 'true' : nothing}
|
|
259
333
|
?disabled=${this.isDisabled}
|
|
260
334
|
@input=${this.handleInput}
|
|
261
335
|
@change=${this.handleChange}
|
|
336
|
+
@keydown=${this.handleKeydown}
|
|
262
337
|
/>`}
|
|
263
338
|
${this.markers > 1
|
|
264
339
|
? html `<div part="markers" aria-hidden="true">
|
|
@@ -271,16 +346,21 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
|
|
|
271
346
|
>`)}
|
|
272
347
|
</div>`
|
|
273
348
|
: nothing}
|
|
274
|
-
${this.renderTooltips(
|
|
349
|
+
${this.renderThumbs()} ${this.renderTooltips()}
|
|
275
350
|
${this.showValue
|
|
276
351
|
? html `<output
|
|
277
352
|
part="output"
|
|
278
|
-
for=${this.range ? '
|
|
353
|
+
for=${this.range ? ids.join(' ') : 'control'}
|
|
279
354
|
aria-hidden="true"
|
|
280
355
|
style=${`--_slider-output-reserve: ${this.reserve}ch`}
|
|
281
|
-
>${this.range
|
|
282
|
-
?
|
|
283
|
-
: this.
|
|
356
|
+
>${!this.range
|
|
357
|
+
? this.said(this.value)
|
|
358
|
+
: this.points.length > 2
|
|
359
|
+
? new Intl.ListFormat(wordsLocale(this), { type: 'conjunction' }).format(this.points.map((at) => this.said(at)))
|
|
360
|
+
: formatWords(this, 'rangeText', {
|
|
361
|
+
end: this.said(end),
|
|
362
|
+
start: this.said(start),
|
|
363
|
+
})}</output
|
|
284
364
|
>`
|
|
285
365
|
: nothing}
|
|
286
366
|
</div>
|
|
@@ -332,12 +412,19 @@ __decorate([
|
|
|
332
412
|
words,
|
|
333
413
|
property({ attribute: 'range-text' })
|
|
334
414
|
], HelixHeadlessSlider.prototype, "rangeText", void 0);
|
|
415
|
+
__decorate([
|
|
416
|
+
property({ attribute: 'readonly', reflect: true, type: Boolean })
|
|
417
|
+
], HelixHeadlessSlider.prototype, "readOnly", void 0);
|
|
335
418
|
__decorate([
|
|
336
419
|
property({ attribute: 'show-value', reflect: true, type: Boolean })
|
|
337
420
|
], HelixHeadlessSlider.prototype, "showValue", void 0);
|
|
338
421
|
__decorate([
|
|
339
422
|
property({ type: Number })
|
|
340
423
|
], HelixHeadlessSlider.prototype, "step", void 0);
|
|
424
|
+
__decorate([
|
|
425
|
+
words,
|
|
426
|
+
property({ attribute: 'thumb-label' })
|
|
427
|
+
], HelixHeadlessSlider.prototype, "thumbLabel", void 0);
|
|
341
428
|
__decorate([
|
|
342
429
|
property({ type: Boolean })
|
|
343
430
|
], HelixHeadlessSlider.prototype, "tooltip", void 0);
|
|
@@ -19,6 +19,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
19
19
|
* Put a `Kbd` in the `shortcut` slot to draw it as keys instead; the dial
|
|
20
20
|
* imports no `Kbd` of its own. Showing a shortcut does not bind it.
|
|
21
21
|
*
|
|
22
|
+
* `href` makes the action a link, as Menu's rows take one: the `item` part is
|
|
23
|
+
* then an `<a>` carrying `menuitem`, so the browser follows it, opens it in a
|
|
24
|
+
* new tab on a middle press, and offers its own menu for it. Choosing it still
|
|
25
|
+
* sends `helix-select` and closes the dial. A disabled action draws no link.
|
|
26
|
+
*
|
|
22
27
|
* @since 0.1.0
|
|
23
28
|
* @status stable
|
|
24
29
|
* @category actions
|
|
@@ -32,7 +37,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
32
37
|
* @event {HelixSelectEvent} helix-select - The action was chosen. Cancelable: cancel it to keep the dial open.
|
|
33
38
|
*
|
|
34
39
|
* @csspart icon - The box that holds the icon.
|
|
35
|
-
* @csspart item - The control for the action.
|
|
40
|
+
* @csspart item - The control for the action, which is an `<a>` where `href` is set.
|
|
36
41
|
* @csspart label - The action's name, beside the icon.
|
|
37
42
|
* @csspart shortcut - The box after the name that holds the shortcut, shown while `shortcut` is set or something is in the `shortcut` slot.
|
|
38
43
|
*/
|
|
@@ -41,12 +46,30 @@ export declare class HelixHeadlessSpeedDialItem extends HelixElement {
|
|
|
41
46
|
* Whether the action is disabled, so it cannot be chosen.
|
|
42
47
|
*/
|
|
43
48
|
disabled: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the link downloads the file when the action is chosen. Only
|
|
51
|
+
* meaningful beside an `href`, and only for a file served from the page's
|
|
52
|
+
* origin.
|
|
53
|
+
*/
|
|
54
|
+
download: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The URL the action links to. Setting it draws the action as a link.
|
|
57
|
+
*/
|
|
58
|
+
href: string;
|
|
59
|
+
/**
|
|
60
|
+
* The link's `rel`. Set it whenever you set `target="_blank"`.
|
|
61
|
+
*/
|
|
62
|
+
rel: string;
|
|
44
63
|
/**
|
|
45
64
|
* The action's keyboard shortcut as a chord, such as `mod+s`, carried as
|
|
46
65
|
* `aria-keyshortcuts` and drawn after the name. `mod` is Command on an Apple
|
|
47
66
|
* keyboard and Control elsewhere. Showing a shortcut does not bind it.
|
|
48
67
|
*/
|
|
49
68
|
shortcut: string;
|
|
69
|
+
/**
|
|
70
|
+
* Where the link opens, such as `_blank` for a new tab.
|
|
71
|
+
*/
|
|
72
|
+
target: string;
|
|
50
73
|
/**
|
|
51
74
|
* The action's value, sent in `helix-select` when the action is chosen.
|
|
52
75
|
*/
|
|
@@ -86,6 +109,11 @@ export declare class HelixHeadlessSpeedDialItem extends HelixElement {
|
|
|
86
109
|
private readonly readShortcut;
|
|
87
110
|
private choose;
|
|
88
111
|
private readonly handleClick;
|
|
112
|
+
/**
|
|
113
|
+
* Space presses a linked action, which an anchor ignores, and the click that
|
|
114
|
+
* comes back both announces and follows the link. Enter is the anchor's own.
|
|
115
|
+
*/
|
|
116
|
+
private readonly handleKeydown;
|
|
89
117
|
protected render(): TemplateResult;
|
|
90
118
|
/**
|
|
91
119
|
* The `shortcut` slot, holding the shortcut drawn after the name as Kbd
|
|
@@ -12,7 +12,11 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
12
12
|
constructor() {
|
|
13
13
|
super(...arguments);
|
|
14
14
|
this.disabled = false;
|
|
15
|
+
this.download = false;
|
|
16
|
+
this.href = '';
|
|
17
|
+
this.rel = '';
|
|
15
18
|
this.shortcut = '';
|
|
19
|
+
this.target = '';
|
|
16
20
|
this.value = '';
|
|
17
21
|
this.isSpeedDialItem = true;
|
|
18
22
|
this.hasShortcut = false;
|
|
@@ -22,6 +26,16 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
22
26
|
this.handleClick = () => {
|
|
23
27
|
this.choose();
|
|
24
28
|
};
|
|
29
|
+
this.handleKeydown = (event) => {
|
|
30
|
+
if (event.key !== ' ') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const link = this.renderRoot.querySelector('a[part="item"]');
|
|
34
|
+
if (link) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
link.click();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
25
39
|
}
|
|
26
40
|
focus(options) {
|
|
27
41
|
this.partNamed('item')?.focus(options);
|
|
@@ -55,6 +69,27 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
55
69
|
}
|
|
56
70
|
render() {
|
|
57
71
|
const keys = this.shortcut === '' ? nothing : ariaChord(this.shortcut, !this.hydrating && onApple());
|
|
72
|
+
const inside = html `<span part="icon"><slot name="icon"></slot></span>
|
|
73
|
+
<span part="label"><slot></slot><span
|
|
74
|
+
part="shortcut"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
?hidden=${!this.hasShortcut && this.shortcut === ''}
|
|
77
|
+
>${this.renderShortcut()}</span
|
|
78
|
+
></span>`;
|
|
79
|
+
if (this.href !== '' && !this.disabled) {
|
|
80
|
+
return html `<a
|
|
81
|
+
part="item"
|
|
82
|
+
href=${this.href}
|
|
83
|
+
target=${this.target || nothing}
|
|
84
|
+
rel=${this.rel || nothing}
|
|
85
|
+
?download=${this.download}
|
|
86
|
+
role="menuitem"
|
|
87
|
+
aria-keyshortcuts=${keys}
|
|
88
|
+
tabindex="-1"
|
|
89
|
+
@click=${this.handleClick}
|
|
90
|
+
@keydown=${this.handleKeydown}
|
|
91
|
+
>${inside}</a>`;
|
|
92
|
+
}
|
|
58
93
|
return html `<button
|
|
59
94
|
part="item"
|
|
60
95
|
role="menuitem"
|
|
@@ -65,13 +100,7 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
65
100
|
tabindex="-1"
|
|
66
101
|
@click=${this.handleClick}
|
|
67
102
|
>
|
|
68
|
-
|
|
69
|
-
<span part="label"><slot></slot><span
|
|
70
|
-
part="shortcut"
|
|
71
|
-
aria-hidden="true"
|
|
72
|
-
?hidden=${!this.hasShortcut && this.shortcut === ''}
|
|
73
|
-
>${this.renderShortcut()}</span
|
|
74
|
-
></span>
|
|
103
|
+
${inside}
|
|
75
104
|
</button>`;
|
|
76
105
|
}
|
|
77
106
|
renderShortcut() {
|
|
@@ -89,9 +118,21 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
89
118
|
__decorate([
|
|
90
119
|
property({ reflect: true, type: Boolean })
|
|
91
120
|
], HelixHeadlessSpeedDialItem.prototype, "disabled", void 0);
|
|
121
|
+
__decorate([
|
|
122
|
+
property({ type: Boolean })
|
|
123
|
+
], HelixHeadlessSpeedDialItem.prototype, "download", void 0);
|
|
124
|
+
__decorate([
|
|
125
|
+
property()
|
|
126
|
+
], HelixHeadlessSpeedDialItem.prototype, "href", void 0);
|
|
127
|
+
__decorate([
|
|
128
|
+
property()
|
|
129
|
+
], HelixHeadlessSpeedDialItem.prototype, "rel", void 0);
|
|
92
130
|
__decorate([
|
|
93
131
|
property()
|
|
94
132
|
], HelixHeadlessSpeedDialItem.prototype, "shortcut", void 0);
|
|
133
|
+
__decorate([
|
|
134
|
+
property()
|
|
135
|
+
], HelixHeadlessSpeedDialItem.prototype, "target", void 0);
|
|
95
136
|
__decorate([
|
|
96
137
|
property()
|
|
97
138
|
], HelixHeadlessSpeedDialItem.prototype, "value", void 0);
|
|
@@ -25,8 +25,9 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
|
|
|
25
25
|
* action whose name starts with it. The actions are one tab stop, and Tab
|
|
26
26
|
* closes the dial and moves on.
|
|
27
27
|
*
|
|
28
|
-
* **Keyboard.** On the trigger, Enter, Space or a press opens the dial
|
|
29
|
-
* first action
|
|
28
|
+
* **Keyboard.** On the trigger, Enter, Space, Down or a press opens the dial
|
|
29
|
+
* on its first action, and Up opens it on its last, as an APG menu button
|
|
30
|
+
* does. With the dial open:
|
|
30
31
|
*
|
|
31
32
|
* | Key | What it does |
|
|
32
33
|
* |---|---|
|
|
@@ -50,6 +51,13 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
|
|
|
50
51
|
* and a slotted one alike. A press that has opened the dial does not also do
|
|
51
52
|
* the trigger's action.
|
|
52
53
|
*
|
|
54
|
+
* **Open on hover.** With `open-on="hover"`, a mouse resting on the trigger
|
|
55
|
+
* opens the dial, as Material's speed dial does, and the dial closes a moment
|
|
56
|
+
* after the mouse leaves both the trigger and the actions. A press, a touch
|
|
57
|
+
* and the keyboard open it as under `click`, and a press on the trigger of a
|
|
58
|
+
* dial already open moves focus to its first action, so the press a mouse
|
|
59
|
+
* makes after hovering keeps the dial open.
|
|
60
|
+
*
|
|
53
61
|
* @since 0.1.0
|
|
54
62
|
* @status stable
|
|
55
63
|
* @category actions
|
|
@@ -57,6 +65,7 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
|
|
|
57
65
|
* @documentation https://fusion.dev/helix/components/speed-dial
|
|
58
66
|
*
|
|
59
67
|
* @slot - The actions.
|
|
68
|
+
* @slot icon - Replaces the plus on the button the dial draws when no trigger is slotted, such as with a pencil for a dial of ways to write.
|
|
60
69
|
* @slot trigger - The control that opens the actions. Without one, a button named by `label` is drawn.
|
|
61
70
|
*
|
|
62
71
|
* @event {HelixBeforeCloseEvent} helix-before-close - The actions are about to close. Cancelable: cancel it to keep them open.
|
|
@@ -94,11 +103,13 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
|
|
|
94
103
|
*/
|
|
95
104
|
offset: number;
|
|
96
105
|
/**
|
|
97
|
-
* What opens the dial. `click` opens it on a press of the trigger.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
106
|
+
* What opens the dial. `click` opens it on a press of the trigger. `hover`
|
|
107
|
+
* also opens it while a mouse rests on the trigger, and closes it once the
|
|
108
|
+
* mouse has left. Under `long-press`, a press is the trigger's own action,
|
|
109
|
+
* and the dial opens on a press held for half a second, or on Alt+Down or
|
|
110
|
+
* Alt+Up.
|
|
100
111
|
*/
|
|
101
|
-
openOn: 'click' | 'long-press';
|
|
112
|
+
openOn: 'click' | 'hover' | 'long-press';
|
|
102
113
|
/**
|
|
103
114
|
* The direction the actions open in. They move to another side if that side
|
|
104
115
|
* does not fit.
|
|
@@ -117,6 +128,10 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
|
|
|
117
128
|
* wherever the reader is typing.
|
|
118
129
|
*/
|
|
119
130
|
private focusOnOpen;
|
|
131
|
+
/** Whether the open under way puts focus on the last action, from Up. */
|
|
132
|
+
private focusLast;
|
|
133
|
+
/** The close a mouse leaving a hover dial has asked for, a moment from now. */
|
|
134
|
+
private leaving;
|
|
120
135
|
protected readonly panel: PopoverController;
|
|
121
136
|
/** Whatever the consumer slotted, or the button rendered in its place. */
|
|
122
137
|
private get trigger();
|
|
@@ -128,6 +143,17 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
|
|
|
128
143
|
/** Watches the trigger for a held press, under `open-on="long-press"`. */
|
|
129
144
|
private readonly longPress;
|
|
130
145
|
private get pressHeld();
|
|
146
|
+
/** Opens on a mouse coming over the trigger, under `open-on="hover"`. */
|
|
147
|
+
private readonly handleTriggerPointerOver;
|
|
148
|
+
/** Keeps a hover dial open while the mouse is back over it. */
|
|
149
|
+
private readonly handlePointerEnter;
|
|
150
|
+
/**
|
|
151
|
+
* Closes a hover dial a moment after the mouse leaves it, so a mouse
|
|
152
|
+
* crossing the gap between the trigger and the actions keeps it open.
|
|
153
|
+
* Focus inside the dial keeps it open too, as a reader is using it.
|
|
154
|
+
*/
|
|
155
|
+
private readonly handlePointerLeave;
|
|
156
|
+
private stay;
|
|
131
157
|
/**
|
|
132
158
|
* Puts focus on the trigger, the slotted one or the button drawn in its
|
|
133
159
|
* place, as a `focus()` on a native control would. The host itself takes no
|
|
@@ -31,6 +31,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
31
31
|
typeahead: typeaheadByWords(),
|
|
32
32
|
});
|
|
33
33
|
this.focusOnOpen = false;
|
|
34
|
+
this.focusLast = false;
|
|
34
35
|
this.panel = new PopoverController(this, {
|
|
35
36
|
anchor: () => this.trigger,
|
|
36
37
|
boundary: () => this,
|
|
@@ -47,7 +48,35 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
});
|
|
51
|
+
this.handleTriggerPointerOver = (event) => {
|
|
52
|
+
if (this.openOn !== 'hover' || event.pointerType !== 'mouse') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.stay();
|
|
56
|
+
if (!this.open) {
|
|
57
|
+
this.requestOpen(true);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
this.handlePointerEnter = () => {
|
|
61
|
+
this.stay();
|
|
62
|
+
};
|
|
63
|
+
this.handlePointerLeave = (event) => {
|
|
64
|
+
if (this.openOn !== 'hover' || event.pointerType !== 'mouse' || !this.open) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.stay();
|
|
68
|
+
this.leaving = setTimeout(() => {
|
|
69
|
+
this.leaving = undefined;
|
|
70
|
+
if (this.open && !this.matches(':focus-within')) {
|
|
71
|
+
this.requestOpen(false);
|
|
72
|
+
}
|
|
73
|
+
}, 300);
|
|
74
|
+
};
|
|
50
75
|
this.handleTriggerClick = () => {
|
|
76
|
+
if (this.open && this.openOn === 'hover') {
|
|
77
|
+
this.items.find((one) => !one.disabled)?.focusItem();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
51
80
|
if (this.open) {
|
|
52
81
|
this.requestOpen(false);
|
|
53
82
|
return;
|
|
@@ -91,11 +120,11 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
91
120
|
};
|
|
92
121
|
this.handleKeydown = (event) => {
|
|
93
122
|
if (!this.open) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
(event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
123
|
+
const arrow = event.key === 'ArrowDown' || event.key === 'ArrowUp';
|
|
124
|
+
if (arrow && (this.pressHeld ? event.altKey : !event.altKey)) {
|
|
97
125
|
event.preventDefault();
|
|
98
126
|
this.focusOnOpen = this.requestOpen(true);
|
|
127
|
+
this.focusLast = this.focusOnOpen && event.key === 'ArrowUp';
|
|
99
128
|
}
|
|
100
129
|
return;
|
|
101
130
|
}
|
|
@@ -132,6 +161,10 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
132
161
|
get pressHeld() {
|
|
133
162
|
return this.openOn === 'long-press';
|
|
134
163
|
}
|
|
164
|
+
stay() {
|
|
165
|
+
clearTimeout(this.leaving);
|
|
166
|
+
this.leaving = undefined;
|
|
167
|
+
}
|
|
135
168
|
focus(options) {
|
|
136
169
|
const trigger = this.trigger;
|
|
137
170
|
if (trigger === this) {
|
|
@@ -151,6 +184,8 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
151
184
|
this.addEventListener('helix-select', this.handleSelect);
|
|
152
185
|
this.addEventListener('keydown', this.handleKeydown);
|
|
153
186
|
this.addEventListener('click', this.handleHeldClick, true);
|
|
187
|
+
this.addEventListener('pointerenter', this.handlePointerEnter);
|
|
188
|
+
this.addEventListener('pointerleave', this.handlePointerLeave);
|
|
154
189
|
}
|
|
155
190
|
disconnectedCallback() {
|
|
156
191
|
super.disconnectedCallback();
|
|
@@ -162,6 +197,9 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
162
197
|
this.removeEventListener('helix-select', this.handleSelect);
|
|
163
198
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
164
199
|
this.removeEventListener('click', this.handleHeldClick, true);
|
|
200
|
+
this.removeEventListener('pointerenter', this.handlePointerEnter);
|
|
201
|
+
this.removeEventListener('pointerleave', this.handlePointerLeave);
|
|
202
|
+
this.stay();
|
|
165
203
|
}
|
|
166
204
|
announce() {
|
|
167
205
|
const trigger = this.trigger;
|
|
@@ -191,11 +229,17 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
191
229
|
super.updated?.(changed);
|
|
192
230
|
this.announce();
|
|
193
231
|
this.describeTrigger();
|
|
232
|
+
if (!this.open) {
|
|
233
|
+
this.stay();
|
|
234
|
+
}
|
|
194
235
|
if (this.open && this.focusOnOpen) {
|
|
195
236
|
this.focusOnOpen = false;
|
|
237
|
+
const last = this.focusLast;
|
|
238
|
+
this.focusLast = false;
|
|
196
239
|
this.updateComplete.then(() => {
|
|
197
240
|
if (this.open) {
|
|
198
|
-
this.items.
|
|
241
|
+
const usable = this.items.filter((one) => !one.disabled);
|
|
242
|
+
(last ? usable.at(-1) : usable[0])?.focusItem();
|
|
199
243
|
}
|
|
200
244
|
});
|
|
201
245
|
}
|
|
@@ -209,6 +253,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
209
253
|
@focusin=${this.handleTriggerFocusIn}
|
|
210
254
|
@pointercancel=${this.handleTriggerPointerCancel}
|
|
211
255
|
@pointerdown=${this.handleTriggerPointerDown}
|
|
256
|
+
@pointerover=${this.handleTriggerPointerOver}
|
|
212
257
|
@pointermove=${this.handleTriggerPointerMove}
|
|
213
258
|
@pointerup=${this.handleTriggerPointerUp}
|
|
214
259
|
@slotchange=${() => this.describeTrigger()}
|
|
@@ -222,7 +267,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
222
267
|
aria-label=${this.label}
|
|
223
268
|
>
|
|
224
269
|
${''}
|
|
225
|
-
|
|
270
|
+
<slot name="icon">${drawIcon(plus)}</slot>
|
|
226
271
|
</button>
|
|
227
272
|
${held ? html `<span id="long-press" hidden>${this.longPressLabel}</span>` : nothing}
|
|
228
273
|
</slot>`;
|