@fusion.dev/helix-headless 0.154.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 +1427 -88
- 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/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/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/navigation/root/navigation-root.d.ts +26 -0
- package/dist/navigation/root/navigation-root.js +58 -1
- package/dist/select/root/select-root.d.ts +29 -3
- package/dist/select/root/select-root.js +34 -9
- 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/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 +984 -151
- package/types/vue.d.ts +221 -49
- package/types/words.d.ts +36 -1
|
@@ -4,7 +4,7 @@ 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, afterLinkFollowed, customElement, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, typesCharacter, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, afterLinkFollowed, customElement, dataOptions, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, sideOf, typesCharacter, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { caretDown } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { nothing } from 'lit';
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
@@ -28,6 +28,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
28
28
|
this.emptyText = 'No matches';
|
|
29
29
|
this.filterable = false;
|
|
30
30
|
this.focusable = true;
|
|
31
|
+
this.items = undefined;
|
|
31
32
|
this.listLabel = 'Options';
|
|
32
33
|
this.loading = false;
|
|
33
34
|
this.loadingText = 'Loading';
|
|
@@ -43,6 +44,11 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
43
44
|
],
|
|
44
45
|
offset: () => 4,
|
|
45
46
|
onDismiss: () => this.close(),
|
|
47
|
+
onPlace: (placement) => {
|
|
48
|
+
const side = sideOf(placement);
|
|
49
|
+
this.toggleState('placed-bottom', side === 'bottom');
|
|
50
|
+
this.toggleState('placed-top', side === 'top');
|
|
51
|
+
},
|
|
46
52
|
open: () => this.open,
|
|
47
53
|
place: () => this.position !== 'item-aligned',
|
|
48
54
|
placement: 'bottom-start',
|
|
@@ -124,7 +130,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
124
130
|
const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
|
125
131
|
if (step !== 0 && !this.multiple && !(event.altKey || event.ctrlKey || event.metaKey)) {
|
|
126
132
|
event.preventDefault();
|
|
127
|
-
const reachable =
|
|
133
|
+
const reachable = this.items
|
|
134
|
+
? this.entries.filter((option) => !option.disabled)
|
|
135
|
+
: (listbox?.options.filter((option) => !option.hidden && !option.disabled) ?? []);
|
|
128
136
|
const next = reachable[Math.max(0, reachable.findIndex((one) => one.value === this.value) + step)];
|
|
129
137
|
if (next) {
|
|
130
138
|
this.choose(next.value);
|
|
@@ -165,16 +173,29 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
165
173
|
return this.partNamed('listbox');
|
|
166
174
|
}
|
|
167
175
|
get display() {
|
|
168
|
-
const
|
|
176
|
+
const chosen = new Set(this.value === '' ? [] : this.value.split(','));
|
|
177
|
+
const words = this.entries.filter((option) => chosen.has(option.value)).map((one) => one.text);
|
|
169
178
|
if (words.length > 0) {
|
|
170
179
|
return new Intl.ListFormat(wordsLocale(this), { style: 'short', type: 'unit' }).format(words);
|
|
171
180
|
}
|
|
172
|
-
return this.value !== '' && !this.childrenKnown ? nothing : this.placeholder;
|
|
181
|
+
return this.value !== '' && !this.childrenKnown && !this.items ? nothing : this.placeholder;
|
|
173
182
|
}
|
|
174
183
|
get chosenOptions() {
|
|
184
|
+
if (this.items) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
175
187
|
const chosen = new Set(this.value === '' ? [] : this.value.split(','));
|
|
176
188
|
return optionsAmong(this.knownChildren).filter((option) => chosen.has(option.value));
|
|
177
189
|
}
|
|
190
|
+
get entries() {
|
|
191
|
+
if (!this.items) {
|
|
192
|
+
return optionsAmong(this.knownChildren);
|
|
193
|
+
}
|
|
194
|
+
if (this.data?.from !== this.items) {
|
|
195
|
+
this.data = { from: this.items, options: dataOptions(this.items) };
|
|
196
|
+
}
|
|
197
|
+
return this.data.options;
|
|
198
|
+
}
|
|
178
199
|
renderValue() {
|
|
179
200
|
const chosen = this.chosenOptions;
|
|
180
201
|
return this.valueContent && chosen.length > 0 ? this.valueContent(chosen) : this.display;
|
|
@@ -226,9 +247,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
226
247
|
}
|
|
227
248
|
}
|
|
228
249
|
reconcileOptions() {
|
|
229
|
-
const options =
|
|
230
|
-
if (this.defaultValue === '' && !this.hasAttribute('default-value')) {
|
|
231
|
-
const declared =
|
|
250
|
+
const options = this.entries;
|
|
251
|
+
if (!this.items && this.defaultValue === '' && !this.hasAttribute('default-value')) {
|
|
252
|
+
const declared = optionsAmong(this.knownChildren)
|
|
232
253
|
.filter((option) => option.defaultSelected)
|
|
233
254
|
.map((option) => option.value);
|
|
234
255
|
if (declared.length > 0) {
|
|
@@ -255,7 +276,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
255
276
|
willUpdate(changed) {
|
|
256
277
|
super.willUpdate(changed);
|
|
257
278
|
hostAria(this.internals, { label: this.label });
|
|
258
|
-
if (this.childrenKnown) {
|
|
279
|
+
if (this.childrenKnown || this.items) {
|
|
259
280
|
this.reconcileOptions();
|
|
260
281
|
}
|
|
261
282
|
}
|
|
@@ -298,6 +319,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
298
319
|
.value=${this.value}
|
|
299
320
|
?multiple=${this.multiple}
|
|
300
321
|
?filterable=${this.filterable}
|
|
322
|
+
.items=${this.items}
|
|
301
323
|
manual
|
|
302
324
|
aria-label=${this.listName || this.listLabel || nothing}
|
|
303
325
|
@keydown=${this.handleListKeydown}
|
|
@@ -357,7 +379,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
|
|
|
357
379
|
disabled: this.isDisabled,
|
|
358
380
|
multiple: this.multiple,
|
|
359
381
|
onFill: (value) => this.requestValue(value),
|
|
360
|
-
options:
|
|
382
|
+
options: this.entries,
|
|
361
383
|
value: this.value,
|
|
362
384
|
})}`;
|
|
363
385
|
}
|
|
@@ -375,6 +397,9 @@ __decorate([
|
|
|
375
397
|
__decorate([
|
|
376
398
|
property({ attribute: false })
|
|
377
399
|
], HelixHeadlessSelect.prototype, "focusable", void 0);
|
|
400
|
+
__decorate([
|
|
401
|
+
property({ attribute: false })
|
|
402
|
+
], HelixHeadlessSelect.prototype, "items", void 0);
|
|
378
403
|
__decorate([
|
|
379
404
|
words,
|
|
380
405
|
property({ attribute: 'list-label' })
|
|
@@ -19,7 +19,12 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
|
|
|
19
19
|
* does not draw a hundred and one marks by accident.
|
|
20
20
|
*
|
|
21
21
|
* A range with two thumbs is two inputs stacked on one track, because there is
|
|
22
|
-
* no native two-thumb range. Each thumb keeps its own value
|
|
22
|
+
* no native two-thumb range. Each thumb keeps its own value, and a named range
|
|
23
|
+
* submits one entry per thumb under its name, the lowest first. A range given
|
|
24
|
+
* more than two values, such as `default-value="20,50,80"`, has a thumb for
|
|
25
|
+
* each, as React Aria's slider takes any number of thumbs: each stops at its
|
|
26
|
+
* neighbors, and the ones between the first and the last are named by
|
|
27
|
+
* `thumb-label`.
|
|
23
28
|
* `min-steps-between-thumbs` keeps them that many steps apart, and the readout
|
|
24
29
|
* joins the two as `range-text` writes them.
|
|
25
30
|
*
|
|
@@ -50,6 +55,8 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
|
|
|
50
55
|
* | Page Up, Page Down | Moves the thumb by a larger step. |
|
|
51
56
|
* | Home, End | Moves the thumb to its lowest or highest value. |
|
|
52
57
|
*
|
|
58
|
+
* Under `readonly` none of these move the thumb, and Tab still reaches it.
|
|
59
|
+
*
|
|
53
60
|
* Each thumb is an `<input type="range">` with `aria-valuetext` where the value
|
|
54
61
|
* is formatted or worded. A range's thumbs say they stop at each other with
|
|
55
62
|
* `aria-valuemin` and `aria-valuemax`.
|
|
@@ -63,12 +70,14 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
|
|
|
63
70
|
* @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
|
|
64
71
|
* @slot hint - Help under the control, for content richer than the `hint` text.
|
|
65
72
|
* @slot label - The label, for content richer than the `label` text.
|
|
73
|
+
* @slot thumb - Content drawn on the thumb, such as a glyph: a single slider's thumb, or a range's lower one. It is decoration, hidden from a screen reader.
|
|
74
|
+
* @slot thumb-end - Content drawn on a range's upper thumb.
|
|
66
75
|
*
|
|
67
76
|
* @event {Event} change - Fired when the user releases the thumb.
|
|
68
77
|
* @event {HelixBeforeChangeEvent} helix-before-change - Fired as the value changes. Cancelable: call `preventDefault()` to keep the current value.
|
|
69
78
|
* @event {Event} input - Fired after the value changes.
|
|
70
79
|
*
|
|
71
|
-
* @csspart control - The range input.
|
|
80
|
+
* @csspart control - The range input, one per thumb. On a range each carries `data-thumb`, `start`, `middle`, or `end`, and `data-index`, its place from the lowest.
|
|
72
81
|
* @csspart feedback - The error message under the track.
|
|
73
82
|
* @csspart field - The row that holds the control and its readout.
|
|
74
83
|
* @csspart hint - The help text under the track, shown while there is no error.
|
|
@@ -77,10 +86,12 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
|
|
|
77
86
|
* @csspart marker-label - The value a mark stands at, when `marker-labels` is set.
|
|
78
87
|
* @csspart markers - The row of marks along the track.
|
|
79
88
|
* @csspart output - The current value, shown beside the track.
|
|
80
|
-
* @csspart
|
|
89
|
+
* @csspart thumb - One thumb's own box over the track, which holds the `thumb` or `thumb-end` slot. It carries `data-thumb` on a range, `start`, `middle`, or `end`, and `--slider-at`, where its thumb is as a fraction of the track. Draw your own thumb here and hide the native one, or leave it empty.
|
|
90
|
+
* @csspart thumbs - The layer over the track that holds each `thumb`. It is `hidden` until a thumb slot has content, and then `[part="field"]` carries `data-thumbs`.
|
|
91
|
+
* @csspart tooltip - One thumb's value in a bubble beside it, under `tooltip`. It carries `data-thumb` on a range, `start`, `middle`, or `end`, and `--slider-at`, where its thumb is as a fraction of the track.
|
|
81
92
|
* @csspart tooltips - The layer over the track that holds the bubbles, under `tooltip`.
|
|
82
93
|
*
|
|
83
|
-
* @cssproperty --slider-at - Where a bubble's thumb is, as a fraction of the track, 0 to 1. Set on each `tooltip` part.
|
|
94
|
+
* @cssproperty --slider-at - Where a bubble's or a thumb box's thumb is, as a fraction of the track, 0 to 1. Set on each `tooltip` and `thumb` part.
|
|
84
95
|
* @cssproperty --slider-fraction - The value as a fraction of the range, 0 to 1.
|
|
85
96
|
* @cssproperty --slider-from - Where the filled span starts, as a fraction of the track, 0 to 1: a range's lower thumb, or a single thumb's `origin`.
|
|
86
97
|
* @cssproperty --slider-marker-at - Where one marker sits, as a percentage of the track.
|
|
@@ -185,9 +196,11 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
|
|
|
185
196
|
/**
|
|
186
197
|
* Adds a second thumb, so the value is a pair.
|
|
187
198
|
*
|
|
188
|
-
* The pair is one string, `"10,50"`, which is what
|
|
189
|
-
* `MixinControllerValue`
|
|
190
|
-
*
|
|
199
|
+
* The pair is one string, `"10,50"`, which is what `value` and
|
|
200
|
+
* `MixinControllerValue` hold. A form receives two entries under the name,
|
|
201
|
+
* `price=10&price=50`, as React Aria's two thumbs send theirs.
|
|
202
|
+
* `valueAsNumber` reads the lower of the two, since a range has no single
|
|
203
|
+
* number.
|
|
191
204
|
*/
|
|
192
205
|
range: boolean;
|
|
193
206
|
/**
|
|
@@ -195,12 +208,27 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
|
|
|
195
208
|
* filled in. Write it in the page's language, such as `{start} bis {end}`.
|
|
196
209
|
*/
|
|
197
210
|
rangeText: string;
|
|
211
|
+
/**
|
|
212
|
+
* Whether a reader can see and focus the value and cannot change it. A
|
|
213
|
+
* read-only slider still submits its value.
|
|
214
|
+
*
|
|
215
|
+
* React Aria's `isReadOnly`. Each thumb stays a tab stop and says
|
|
216
|
+
* `aria-readonly`. The arrow keys, the page keys, Home, End, and a drag
|
|
217
|
+
* leave the value where it is. A script can still set `value`.
|
|
218
|
+
*/
|
|
219
|
+
readOnly: boolean;
|
|
198
220
|
/**
|
|
199
221
|
* Shows the chosen value beside the track.
|
|
200
222
|
*/
|
|
201
223
|
showValue: boolean;
|
|
202
224
|
/** How far one press of an arrow key moves the thumb. */
|
|
203
225
|
step: number;
|
|
226
|
+
/**
|
|
227
|
+
* The name of a thumb between a range's first and last, read after the
|
|
228
|
+
* slider's label, with `{index}` filled in: its place, counted from 1. The
|
|
229
|
+
* first is `minimum-label` and the last `maximum-label`.
|
|
230
|
+
*/
|
|
231
|
+
thumbLabel: string;
|
|
204
232
|
/**
|
|
205
233
|
* Whether each thumb shows its value in a bubble beside it, as Web
|
|
206
234
|
* Awesome's `with-tooltip`. The styled slider shows the bubbles while the
|
|
@@ -258,12 +286,29 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
|
|
|
258
286
|
* is what a filter with nothing filtered out looks like.
|
|
259
287
|
*/
|
|
260
288
|
private get bounds();
|
|
289
|
+
/**
|
|
290
|
+
* A range's values, one per thumb, as numbers in order.
|
|
291
|
+
*
|
|
292
|
+
* A lone number is not a range, so it is not read as one. `willUpdate`
|
|
293
|
+
* seeds and sanitizes a range to two values or more, and anything else that
|
|
294
|
+
* arrives between the two opens fully wide, which is what "nothing filtered
|
|
295
|
+
* out" looks like.
|
|
296
|
+
*/
|
|
297
|
+
private get points();
|
|
261
298
|
/** Where one value sits along the track, as a fraction of it. */
|
|
262
299
|
private along;
|
|
263
300
|
private readonly handleInput;
|
|
264
301
|
/** Relayed, because `change` does not cross a shadow boundary on its own. */
|
|
265
302
|
private readonly handleChange;
|
|
266
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* Keeps a read-only thumb still under the keys that move it. Tab and every
|
|
305
|
+
* other key pass, so focus moves on.
|
|
306
|
+
*/
|
|
307
|
+
private readonly handleKeydown;
|
|
308
|
+
/**
|
|
309
|
+
* Reports the value: one entry, or for a named range one entry per thumb.
|
|
310
|
+
* The joined pair is the state the browser restores.
|
|
311
|
+
*/
|
|
267
312
|
private sync;
|
|
268
313
|
/**
|
|
269
314
|
* How far along the track the thumb sits, as a fraction.
|
|
@@ -325,18 +370,35 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
|
|
|
325
370
|
/** What a screen reader says for one thumb, where `value-text` asks. */
|
|
326
371
|
private spoken;
|
|
327
372
|
protected updated(changed: PropertyValues): void;
|
|
373
|
+
/** Draws, once a served slider has hydrated, what its children ask for. */
|
|
374
|
+
protected firstUpdated(changed: PropertyValues): void;
|
|
328
375
|
/**
|
|
329
376
|
* What a screen reader says for a thumb's value, with `{value}` filled in,
|
|
330
377
|
* such as `{value} degrees`. Empty announces the number alone.
|
|
331
378
|
*/
|
|
332
379
|
valueText: string;
|
|
380
|
+
/**
|
|
381
|
+
* Whether a `thumb` or `thumb-end` slot has content, read off the children
|
|
382
|
+
* rather than heard from `slotchange`: a declarative shadow root fires that
|
|
383
|
+
* before anything listens, so a served slider kept its layer hidden.
|
|
384
|
+
*/
|
|
385
|
+
private get thumbContent();
|
|
386
|
+
private readonly handleThumbSlot;
|
|
387
|
+
/**
|
|
388
|
+
* A box for each thumb, over the track at the thumb's place, holding the
|
|
389
|
+
* `thumb` slot: a range's upper thumb holds `thumb-end`. Hidden from the
|
|
390
|
+
* accessibility tree, since each thumb is already a range with a name and a
|
|
391
|
+
* value, and `hidden` while no slot has content, so an empty layer takes no
|
|
392
|
+
* room in the row.
|
|
393
|
+
*/
|
|
394
|
+
protected renderThumbs(): TemplateResult;
|
|
333
395
|
/**
|
|
334
396
|
* A bubble for each thumb, under `tooltip`, saying its value where it is.
|
|
335
397
|
* After the thumbs, so a stylesheet can reach a bubble from a focused
|
|
336
398
|
* thumb, and hidden from the accessibility tree for the reason the readout
|
|
337
399
|
* is: each thumb already reports its value.
|
|
338
400
|
*/
|
|
339
|
-
protected renderTooltips(
|
|
401
|
+
protected renderTooltips(): TemplateResult | typeof nothing;
|
|
340
402
|
protected render(): TemplateResult;
|
|
341
403
|
}
|
|
342
404
|
declare global {
|
|
@@ -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
|