@fusion.dev/helix-headless 0.163.0 → 0.165.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 +1068 -46
- package/dist/callout/root/callout-root.d.ts +43 -4
- package/dist/callout/root/callout-root.js +58 -2
- package/dist/code-block/patch.d.ts +42 -0
- package/dist/code-block/patch.js +118 -0
- package/dist/code-block/root/code-block-root.d.ts +37 -1
- package/dist/code-block/root/code-block-root.js +57 -9
- package/dist/color-picker/root/color-picker-root.d.ts +7 -3
- package/dist/color-picker/root/color-picker-root.js +7 -4
- package/dist/combobox/root/combobox-root.d.ts +1 -0
- package/dist/combobox/root/combobox-root.js +10 -1
- package/dist/data-table/root/data-table-root.d.ts +1 -0
- package/dist/data-table/root/data-table-root.js +1 -0
- package/dist/date-input/root/date-input-root.d.ts +3 -3
- package/dist/date-input/root/date-input-root.js +8 -5
- package/dist/element-events.generated.js +7 -1
- package/dist/format-date/root/format-date-root.d.ts +3 -1
- package/dist/format-date/root/format-date-root.js +4 -1
- package/dist/lightbox/item/lightbox-item.js +3 -1
- package/dist/lightbox/root/lightbox-root.d.ts +28 -0
- package/dist/lightbox/root/lightbox-root.js +21 -2
- package/dist/number-input/root/number-input-root.d.ts +32 -9
- package/dist/number-input/root/number-input-root.js +53 -9
- package/dist/object-inspector/root/object-inspector-root.d.ts +23 -0
- package/dist/object-inspector/root/object-inspector-root.js +19 -1
- package/dist/option/root/index.d.ts +1 -0
- package/dist/option/root/index.js +1 -0
- package/dist/table/grid-controller.d.ts +7 -0
- package/dist/table/grid-controller.js +15 -1
- package/dist/table/root/table-root.d.ts +10 -0
- package/dist/table/root/table-root.js +18 -1
- package/dist/tag/root/index.d.ts +1 -0
- package/dist/tag/root/index.js +1 -0
- package/dist/time-input/root/time-input-root.d.ts +4 -3
- package/dist/time-input/root/time-input-root.js +5 -4
- package/dist/time-picker/root/time-picker-root.d.ts +1 -1
- package/dist/time-picker/root/time-picker-root.js +5 -2
- package/dist/video/root/video-root.js +2 -1
- package/dist/video-playlist/item/video-playlist-item.d.ts +8 -0
- package/dist/video-playlist/item/video-playlist-item.js +7 -0
- package/dist/video-playlist/root/video-playlist-root.d.ts +60 -0
- package/dist/video-playlist/root/video-playlist-root.js +55 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +388 -40
- package/types/vue.d.ts +95 -13
- package/types/words.d.ts +58 -0
|
@@ -26,6 +26,7 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
|
|
|
26
26
|
* | Key | What it does |
|
|
27
27
|
* |---|---|
|
|
28
28
|
* | Down, Up | On a closed field, opens the list on the chosen option, or the first or the last. In the open list, moves the highlight, round from the last option to the first. |
|
|
29
|
+
* | Alt with Down, Alt with Up | Opens the list without moving the highlight, and closes it, choosing nothing. |
|
|
29
30
|
* | Page Down, Page Up | In the open list, moves the highlight by as many options as the list shows at once. |
|
|
30
31
|
* | Enter | Chooses the highlighted option. With nothing left in the list, creates what was typed with `creatable`, or takes it with `allows-custom-value`. On a closed field, submits the form. |
|
|
31
32
|
* | Escape | Closes the list and puts the chosen words back. On a closed field, a second Escape clears a single choice. |
|
|
@@ -154,7 +154,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
if (!this.open) {
|
|
157
|
-
if (event.key === 'ArrowDown'
|
|
157
|
+
if (event.key === 'ArrowDown' && event.altKey) {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
this.requestOpen(true);
|
|
160
|
+
}
|
|
161
|
+
else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
158
162
|
event.preventDefault();
|
|
159
163
|
const fallback = event.key === 'ArrowDown' ? 'first' : 'last';
|
|
160
164
|
this.opening = this.values.length > 0 ? 'selected' : fallback;
|
|
@@ -162,6 +166,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
|
|
|
162
166
|
}
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
169
|
+
if (event.key === 'ArrowUp' && event.altKey) {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
this.close();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
165
174
|
if (this.moveAroundCreate(event)) {
|
|
166
175
|
void this.updateComplete.then(() => this.linkActive());
|
|
167
176
|
return;
|
|
@@ -215,6 +215,7 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
|
|
|
215
215
|
*
|
|
216
216
|
* @cssstate empty - While there are no rows to show and none are loading.
|
|
217
217
|
* @cssstate error - While `error-text` is set.
|
|
218
|
+
* @cssstate loading - While `loading` is set, as the rows are on their way.
|
|
218
219
|
*/
|
|
219
220
|
export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base {
|
|
220
221
|
/**
|
|
@@ -984,6 +984,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
|
|
|
984
984
|
this.markEmpty();
|
|
985
985
|
this.toggleState('error', this.errorText !== '');
|
|
986
986
|
this.toggleState('empty', !this.loading && !this.hasRows);
|
|
987
|
+
this.toggleState('loading', this.loading);
|
|
987
988
|
this.assign();
|
|
988
989
|
this.announceSort();
|
|
989
990
|
this.quietControls();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AriaForwardController, type DateCalendar, HelixFormElement, type MonthCalendar, type MonthSystem } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
import '../../date-picker/index.js';
|
|
4
|
-
import '../../tag/index.js';
|
|
4
|
+
import '../../tag/root/index.js';
|
|
5
5
|
declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSegmentedFieldInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinPickerEditingInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinPickerFieldInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
|
|
6
6
|
/**
|
|
7
7
|
* @summary A date, typed or picked from a calendar.
|
|
@@ -474,8 +474,8 @@ export declare class HelixHeadlessDateInput extends HelixHeadlessDateInput_base
|
|
|
474
474
|
/**
|
|
475
475
|
* Each date, where `mode` holds several, sent one entry each.
|
|
476
476
|
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
477
|
+
* Text that is no date, such as `August 5`, is sent as one empty entry, as
|
|
478
|
+
* a native date field sends a date it cannot read, in every mode.
|
|
479
479
|
*/
|
|
480
480
|
protected get answers(): readonly string[] | undefined;
|
|
481
481
|
/** What is wrong with the dates, which the text input cannot say. */
|
|
@@ -11,7 +11,7 @@ import { property, state } from 'lit/decorators.js';
|
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
12
|
import { html } from 'lit/static-html.js';
|
|
13
13
|
import '../../date-picker/index.js';
|
|
14
|
-
import '../../tag/index.js';
|
|
14
|
+
import '../../tag/root/index.js';
|
|
15
15
|
const dates = (value) => value
|
|
16
16
|
.replace(/\s+[–-]\s+/g, ',')
|
|
17
17
|
.split(',')
|
|
@@ -70,9 +70,11 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
|
|
|
70
70
|
};
|
|
71
71
|
this.handleTextCommit = (event) => {
|
|
72
72
|
if (this.mode !== 'multiple') {
|
|
73
|
-
const held = this.mode
|
|
74
|
-
? this.value.
|
|
75
|
-
:
|
|
73
|
+
const held = this.mode !== 'range'
|
|
74
|
+
? [this.value.trim()].filter(Boolean)
|
|
75
|
+
: /\s[–-]\s/.test(this.value)
|
|
76
|
+
? this.value.split(/\s+[–-]\s+/).map((one) => one.trim())
|
|
77
|
+
: dates(this.value);
|
|
76
78
|
const read = held.map((one) => this.read(one));
|
|
77
79
|
if (held.length > 0 && !held.every(isDateValue) && read.every(Boolean)) {
|
|
78
80
|
this.requestValue(read.join(','));
|
|
@@ -220,7 +222,8 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinSegmented
|
|
|
220
222
|
}
|
|
221
223
|
get answers() {
|
|
222
224
|
if (this.mode !== undefined) {
|
|
223
|
-
|
|
225
|
+
const held = dates(this.value);
|
|
226
|
+
return held.every(isDateValue) ? held : [''];
|
|
224
227
|
}
|
|
225
228
|
const text = this.value.trim();
|
|
226
229
|
return text === '' || isDateValue(text) ? undefined : [''];
|
|
@@ -335,7 +335,13 @@ registerElementEvents({
|
|
|
335
335
|
'volumechange',
|
|
336
336
|
'waiting',
|
|
337
337
|
],
|
|
338
|
-
'helix-headless-video-playlist': [
|
|
338
|
+
'helix-headless-video-playlist': [
|
|
339
|
+
'helix-activate',
|
|
340
|
+
'helix-before-activate',
|
|
341
|
+
'helix-before-reorder',
|
|
342
|
+
'helix-end',
|
|
343
|
+
'helix-reorder',
|
|
344
|
+
],
|
|
339
345
|
'helix-headless-video-playlist-item': ['helix-select'],
|
|
340
346
|
'helix-headless-video-playlist-menu': ['helix-select'],
|
|
341
347
|
'helix-headless-virtual-list': [
|
|
@@ -200,7 +200,9 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
|
|
|
200
200
|
* `narrow` ("3h ago").
|
|
201
201
|
*
|
|
202
202
|
* The reading refreshes as it goes stale, as `elapsed` does, and the whole
|
|
203
|
-
* date is its title. `elapsed` wins where both are set.
|
|
203
|
+
* date is its title. `elapsed` wins where both are set. Written with no
|
|
204
|
+
* value, as `elapsed` is, or with a style `Intl` does not know, it reads
|
|
205
|
+
* `long`.
|
|
204
206
|
*/
|
|
205
207
|
relative?: 'long' | 'narrow' | 'short';
|
|
206
208
|
/**
|
|
@@ -10,6 +10,7 @@ import { property } from 'lit/decorators.js';
|
|
|
10
10
|
import { drawFields } from '../fields.js';
|
|
11
11
|
import { formatOptions } from '../options.js';
|
|
12
12
|
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
13
|
+
const STYLES = ['long', 'narrow', 'short'];
|
|
13
14
|
let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement {
|
|
14
15
|
constructor() {
|
|
15
16
|
super(...arguments);
|
|
@@ -53,7 +54,9 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
|
|
|
53
54
|
? elapsedTime(this.language)(moment, now)
|
|
54
55
|
: relativeTime(this.language, {
|
|
55
56
|
numeric: this.numeric ?? 'auto',
|
|
56
|
-
style: this.relative
|
|
57
|
+
style: STYLES.includes(this.relative)
|
|
58
|
+
? this.relative
|
|
59
|
+
: 'long',
|
|
57
60
|
})(moment, now);
|
|
58
61
|
}
|
|
59
62
|
connectedCallback() {
|
|
@@ -47,7 +47,9 @@ __decorate([
|
|
|
47
47
|
property({ type: Boolean })
|
|
48
48
|
], HelixHeadlessLightboxItem.prototype, "current", void 0);
|
|
49
49
|
__decorate([
|
|
50
|
-
property(
|
|
50
|
+
property({
|
|
51
|
+
converter: { fromAttribute: (value) => value ?? '' },
|
|
52
|
+
})
|
|
51
53
|
], HelixHeadlessLightboxItem.prototype, "label", void 0);
|
|
52
54
|
__decorate([
|
|
53
55
|
property({ attribute: false })
|
|
@@ -258,6 +258,32 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
258
258
|
* which is the first time anything asked it for its items on a server.
|
|
259
259
|
*/
|
|
260
260
|
private get items();
|
|
261
|
+
/**
|
|
262
|
+
* The set changed: render it again.
|
|
263
|
+
*
|
|
264
|
+
* `items` is read on each render and nothing else marks a render as due when
|
|
265
|
+
* an item arrives or leaves, so an item appended after the first render had
|
|
266
|
+
* no role description, no place in the set, and no part in the count until
|
|
267
|
+
* some unrelated property changed.
|
|
268
|
+
*/
|
|
269
|
+
private readonly handleSlotChange;
|
|
270
|
+
/**
|
|
271
|
+
* Watches each item's `label` and `value`, which no slot reports.
|
|
272
|
+
*
|
|
273
|
+
* `value` decides which item `active` names, and `label` fills `{label}` in
|
|
274
|
+
* `status-label` and `slide-label`. A change to either left the old item
|
|
275
|
+
* shown, or the old words in the counter, until some unrelated property
|
|
276
|
+
* changed. `Select` watches its options for the same reason.
|
|
277
|
+
*
|
|
278
|
+
* The subtree, since an observer on the host sees no attribute on its
|
|
279
|
+
* children otherwise. Only changes on the items themselves count: a `<progress
|
|
280
|
+
* value>` inside an item, updated while a video plays, would otherwise render
|
|
281
|
+
* the viewer again on each update.
|
|
282
|
+
*
|
|
283
|
+
* Made on connection, there being no `MutationObserver` on a server.
|
|
284
|
+
*/
|
|
285
|
+
private itemWatch;
|
|
286
|
+
connectedCallback(): void;
|
|
261
287
|
/** The native dialog, or nothing where there is no DOM to hold one. */
|
|
262
288
|
private get dialog();
|
|
263
289
|
/** Opens the viewer, which makes the rest of the page inert. */
|
|
@@ -322,6 +348,8 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
322
348
|
private zoomOut;
|
|
323
349
|
/** Home and End, which are an index rather than a step. */
|
|
324
350
|
private toEnd;
|
|
351
|
+
/** The item shown at the last update, whose zoom a different item does not keep. */
|
|
352
|
+
private zoomedItem;
|
|
325
353
|
/** The item on screen: the one `active` names, or the first where it names none. */
|
|
326
354
|
private get shown();
|
|
327
355
|
/**
|
|
@@ -92,6 +92,10 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
92
92
|
'--toggle': () => this.toggleOpen(),
|
|
93
93
|
},
|
|
94
94
|
});
|
|
95
|
+
this.handleSlotChange = () => {
|
|
96
|
+
this.requestUpdate();
|
|
97
|
+
};
|
|
98
|
+
this.itemWatch = undefined;
|
|
95
99
|
this.handleCancel = (event) => {
|
|
96
100
|
event.preventDefault();
|
|
97
101
|
this.requestOpen(false);
|
|
@@ -129,6 +133,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
129
133
|
this.toEnd(this.items.length - 1);
|
|
130
134
|
}
|
|
131
135
|
};
|
|
136
|
+
this.zoomedItem = undefined;
|
|
132
137
|
this.handleBackdrop = (event) => {
|
|
133
138
|
if (this.dragged(event) || !this.onEmptyArea(event)) {
|
|
134
139
|
return;
|
|
@@ -177,6 +182,16 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
177
182
|
get items() {
|
|
178
183
|
return this.knownChildren.filter((child) => child instanceof HelixHeadlessLightboxItem);
|
|
179
184
|
}
|
|
185
|
+
connectedCallback() {
|
|
186
|
+
super.connectedCallback();
|
|
187
|
+
this.itemWatch = new MutationObserver((records) => {
|
|
188
|
+
if (records.some((record) => record.target instanceof HelixHeadlessLightboxItem &&
|
|
189
|
+
record.target.parentElement === this)) {
|
|
190
|
+
this.requestUpdate();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
this.itemWatch.observe(this, { attributeFilter: ['label', 'value'], subtree: true });
|
|
194
|
+
}
|
|
180
195
|
get dialog() {
|
|
181
196
|
return this.renderRoot?.querySelector('dialog') ?? null;
|
|
182
197
|
}
|
|
@@ -292,16 +307,20 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
292
307
|
this.toggleState('fullscreen', this.filled);
|
|
293
308
|
this.toggleState('idle', this.resting);
|
|
294
309
|
this.toggleState('zoomable', this.maxZoom > 1);
|
|
295
|
-
|
|
310
|
+
const shown = this.shown;
|
|
311
|
+
if (shown !== this.zoomedItem || (changed.has('open') && !this.open)) {
|
|
296
312
|
this.quietZoom = true;
|
|
297
313
|
this.zoomer.reset();
|
|
298
314
|
this.quietZoom = false;
|
|
299
315
|
}
|
|
316
|
+
this.zoomedItem = shown;
|
|
300
317
|
this.say(changed);
|
|
301
318
|
}
|
|
302
319
|
disconnectedCallback() {
|
|
303
320
|
super.disconnectedCallback();
|
|
304
321
|
unlockScroll(this);
|
|
322
|
+
this.itemWatch?.disconnect();
|
|
323
|
+
this.itemWatch = undefined;
|
|
305
324
|
}
|
|
306
325
|
updated(changed) {
|
|
307
326
|
super.updated(changed);
|
|
@@ -395,7 +414,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
395
414
|
>
|
|
396
415
|
<slot name="header" part="header"></slot>
|
|
397
416
|
<div part="viewport">
|
|
398
|
-
<slot></slot>
|
|
417
|
+
<slot @slotchange=${this.handleSlotChange}></slot>
|
|
399
418
|
</div>
|
|
400
419
|
<slot name="footer" part="footer"></slot>
|
|
401
420
|
${this.controls ? this.renderControls() : nothing}
|
|
@@ -8,7 +8,7 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
8
8
|
* A separate family from `Input`'s `type="number"`. It renders two buttons,
|
|
9
9
|
* binds four keys `Input` does not, and handles clamping and stepping.
|
|
10
10
|
*
|
|
11
|
-
* The field is `type="text"` with `inputmode
|
|
11
|
+
* The field is `type="text"` with an `inputmode` for a number. Tested in a real
|
|
12
12
|
* browser, `type="number"` fails in three ways:
|
|
13
13
|
* - Typing `12e` gives a `.value` of `""`, so the element cannot see what you
|
|
14
14
|
* typed. A `type="text"` field keeps `"12e"`.
|
|
@@ -16,7 +16,11 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
16
16
|
* to 4, when you only meant to scroll the page.
|
|
17
17
|
* - `selectionStart` is `null`, so the caret cannot be managed.
|
|
18
18
|
*
|
|
19
|
-
* `inputmode
|
|
19
|
+
* `inputmode` keeps a numeric keypad on phones, chosen as React Aria's
|
|
20
|
+
* NumberField chooses it: a keypad with a minus key where the number can be
|
|
21
|
+
* negative, and one with a decimal separator where the format shows a
|
|
22
|
+
* fraction. An iPhone has no keypad with a minus key, so a field that can be
|
|
23
|
+
* negative takes the full keyboard there.
|
|
20
24
|
*
|
|
21
25
|
* `format-options` shows the number with `Intl.NumberFormat` in `locale`, as a
|
|
22
26
|
* currency, a percent or a unit, while the field is not being edited. Focus
|
|
@@ -85,6 +89,8 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
|
|
|
85
89
|
* @csspart increment-mark - The icon on the step up button.
|
|
86
90
|
* @csspart label - The label above the field. It is also the grip you drag when `scrub` is set.
|
|
87
91
|
*
|
|
92
|
+
* @cssstate at-max - The number is at `max`, so it cannot step up, as React Aria's NumberField says with `canIncrement`.
|
|
93
|
+
* @cssstate at-min - The number is at `min`, so it cannot step down.
|
|
88
94
|
* @cssstate invalid - The control has an error to show, from the browser's validation or from your `feedback`.
|
|
89
95
|
* @cssstate scrubbing - The label is being dragged to change the value, under `scrub`, as Zag's scrubber says while it is held.
|
|
90
96
|
*/
|
|
@@ -209,12 +215,13 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
209
215
|
* invalid, as it is in a native number field.
|
|
210
216
|
*
|
|
211
217
|
* Unset, a step moves by one, or by one percent under a percent format, and
|
|
212
|
-
* a typed number is rounded to the smallest amount
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* the
|
|
218
|
+
* a typed number is rounded to the smallest amount the format shows, as
|
|
219
|
+
* React Aria's NumberField does: a field showing dollars keeps the cents
|
|
220
|
+
* typed into it, and one with no `format-options` keeps three decimal
|
|
221
|
+
* places. `any` puts the number on no grid, as it does in a native number
|
|
222
|
+
* field: a step moves by one, and any number in range is valid.
|
|
216
223
|
*/
|
|
217
|
-
step?: number;
|
|
224
|
+
step?: number | 'any';
|
|
218
225
|
/**
|
|
219
226
|
* The message the browser shows when the number is off the step grid, with
|
|
220
227
|
* `{below}` and `{above}` filled in with the nearest valid values, written in
|
|
@@ -251,16 +258,30 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
251
258
|
* has no navigator and renders the spinbutton.
|
|
252
259
|
*/
|
|
253
260
|
private readonly touchApple;
|
|
261
|
+
/** The phone the field is on, read once on the client, for the keypad it offers. */
|
|
262
|
+
private readonly phone;
|
|
263
|
+
/**
|
|
264
|
+
* The keypad a phone shows, as React Aria's NumberField chooses it.
|
|
265
|
+
* `numeric` on Android has a minus key and a decimal separator, and
|
|
266
|
+
* `decimal` has no minus key. On an iPhone neither has a minus key, so a
|
|
267
|
+
* number that can be negative takes the full keyboard. Measured before
|
|
268
|
+
* this: `decimal` with `min="-10"`, so a phone could not type the minus.
|
|
269
|
+
*/
|
|
270
|
+
private get keypad();
|
|
254
271
|
private get control();
|
|
255
272
|
/** The value as a number, or `null` where the field holds nothing usable. */
|
|
256
273
|
private get numeric();
|
|
257
274
|
/** The grain last read off the format, with the locale and options it was read for. */
|
|
258
275
|
private grainOf?;
|
|
276
|
+
/** The step as a number, or zero where it is unset or `any`. */
|
|
277
|
+
private get stepSize();
|
|
259
278
|
/** How far one press moves: `step`, or the stride the format implies. */
|
|
260
279
|
private get stride();
|
|
261
280
|
/**
|
|
262
|
-
* The grid a typed number is rounded to: `step` where it is set,
|
|
263
|
-
* otherwise the smallest amount the format shows
|
|
281
|
+
* The grid a typed number is rounded to: `step` where it is set, nothing
|
|
282
|
+
* where it is `any`, and otherwise the smallest amount the format shows.
|
|
283
|
+
* Measured before this: with no step and no format, `2,75` typed in German
|
|
284
|
+
* became 3 on blur.
|
|
264
285
|
*/
|
|
265
286
|
private get grid();
|
|
266
287
|
/**
|
|
@@ -323,6 +344,8 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
|
|
|
323
344
|
* `handleStepperClick`.
|
|
324
345
|
*/
|
|
325
346
|
private readonly handleStepperDown;
|
|
347
|
+
/** Keeps a pressed stepper from taking focus from the field. */
|
|
348
|
+
private readonly handleStepperMouseDown;
|
|
326
349
|
/**
|
|
327
350
|
* Ends a hold. The click that follows a release arrives in the same task,
|
|
328
351
|
* so the press is forgotten after it.
|
|
@@ -12,6 +12,20 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
12
12
|
import { decimals, formatGrain, strideOf } from '../grain.js';
|
|
13
13
|
const SCRUB_PITCH = 4;
|
|
14
14
|
const SCRUB_FINE = 16;
|
|
15
|
+
const PLAIN_FORMAT = {};
|
|
16
|
+
const stepConverter = {
|
|
17
|
+
fromAttribute: (text) => {
|
|
18
|
+
if (text === null) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return text.trim().toLowerCase() === 'any' ? 'any' : Number(text);
|
|
22
|
+
},
|
|
23
|
+
toAttribute: (step) => step === undefined ? undefined : String(step),
|
|
24
|
+
};
|
|
25
|
+
const phoneOf = (platform) => {
|
|
26
|
+
const agent = platform?.userAgent ?? '';
|
|
27
|
+
return /iPhone|iPod/u.test(agent) ? 'iphone' : /android/iu.test(agent) ? 'android' : '';
|
|
28
|
+
};
|
|
15
29
|
const PLAIN = /^-?(\d+(\.\d*)?|\.\d+)$/;
|
|
16
30
|
const REPEAT_DELAY = 400;
|
|
17
31
|
const REPEAT_EVERY = 60;
|
|
@@ -53,6 +67,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
53
67
|
this.editing = true;
|
|
54
68
|
};
|
|
55
69
|
this.touchApple = appleTouch(typeof navigator === 'undefined' ? undefined : navigator);
|
|
70
|
+
this.phone = phoneOf(typeof navigator === 'undefined' ? undefined : navigator);
|
|
56
71
|
this.pressStepped = false;
|
|
57
72
|
this.handleStepperDown = (event) => {
|
|
58
73
|
if (event.button !== 0) {
|
|
@@ -60,6 +75,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
60
75
|
}
|
|
61
76
|
const button = event.currentTarget;
|
|
62
77
|
const up = button.getAttribute('part') === 'increment';
|
|
78
|
+
if (event.pointerType === 'mouse' && this.control && !this.control.matches(':focus')) {
|
|
79
|
+
this.control.focus({ preventScroll: true });
|
|
80
|
+
}
|
|
63
81
|
clearTimeout(this.repeat);
|
|
64
82
|
this.pressStepped = true;
|
|
65
83
|
button.setPointerCapture?.(event.pointerId);
|
|
@@ -79,6 +97,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
79
97
|
this.repeat = setTimeout(step, REPEAT_DELAY);
|
|
80
98
|
}
|
|
81
99
|
};
|
|
100
|
+
this.handleStepperMouseDown = (event) => {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
};
|
|
82
103
|
this.stopRepeat = () => {
|
|
83
104
|
clearTimeout(this.repeat);
|
|
84
105
|
this.repeat = undefined;
|
|
@@ -217,6 +238,18 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
217
238
|
}
|
|
218
239
|
return formatNumber(parsed, this.locale, this.formatOptions);
|
|
219
240
|
}
|
|
241
|
+
get keypad() {
|
|
242
|
+
const negative = this.min === undefined || this.min < 0;
|
|
243
|
+
const grid = this.grid;
|
|
244
|
+
const fraction = grid === 0 || !Number.isInteger(grid);
|
|
245
|
+
if (this.phone === 'iphone') {
|
|
246
|
+
return negative ? 'text' : fraction ? 'decimal' : 'numeric';
|
|
247
|
+
}
|
|
248
|
+
if (this.phone === 'android') {
|
|
249
|
+
return negative ? 'numeric' : fraction ? 'decimal' : 'numeric';
|
|
250
|
+
}
|
|
251
|
+
return 'numeric';
|
|
252
|
+
}
|
|
220
253
|
get control() {
|
|
221
254
|
return this.partNamed('control');
|
|
222
255
|
}
|
|
@@ -227,15 +260,18 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
227
260
|
}
|
|
228
261
|
return parseNumber(text, this.locale, this.formatOptions);
|
|
229
262
|
}
|
|
263
|
+
get stepSize() {
|
|
264
|
+
return typeof this.step === 'number' ? Math.abs(this.step) || 0 : 0;
|
|
265
|
+
}
|
|
230
266
|
get stride() {
|
|
231
|
-
return
|
|
267
|
+
return this.stepSize || strideOf(this.formatOptions);
|
|
232
268
|
}
|
|
233
269
|
get grid() {
|
|
234
|
-
const step =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return step || 1;
|
|
270
|
+
const step = this.stepSize;
|
|
271
|
+
if (step || this.step === 'any') {
|
|
272
|
+
return step;
|
|
238
273
|
}
|
|
274
|
+
const options = this.formatOptions ?? PLAIN_FORMAT;
|
|
239
275
|
const [locale, read, grain] = this.grainOf ?? [];
|
|
240
276
|
if (locale === this.locale && read === options && grain !== undefined) {
|
|
241
277
|
return grain;
|
|
@@ -252,6 +288,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
252
288
|
}
|
|
253
289
|
onStep(value) {
|
|
254
290
|
const step = this.grid;
|
|
291
|
+
if (!step) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
255
294
|
const anchor = this.min ?? 0;
|
|
256
295
|
const steps = (value - anchor) / step;
|
|
257
296
|
return Math.abs(steps - Math.round(steps)) < 1e-9;
|
|
@@ -259,11 +298,11 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
259
298
|
clamp(value) {
|
|
260
299
|
const step = this.grid;
|
|
261
300
|
const anchor = this.min ?? 0;
|
|
262
|
-
const stepped = anchor + Math.round((value - anchor) / step) * step;
|
|
301
|
+
const stepped = step ? anchor + Math.round((value - anchor) / step) * step : value;
|
|
263
302
|
const floored = this.min === undefined ? stepped : Math.max(this.min, stepped);
|
|
264
303
|
const capped = this.max === undefined ? floored : Math.min(this.max, floored);
|
|
265
304
|
const places = Math.max(decimals(step), decimals(anchor));
|
|
266
|
-
return Number(capped.toFixed(places));
|
|
305
|
+
return step ? Number(capped.toFixed(places)) : capped;
|
|
267
306
|
}
|
|
268
307
|
nudge(steps) {
|
|
269
308
|
if (this.isDisabled || this.readOnly) {
|
|
@@ -387,6 +426,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
387
426
|
this.releaseCaret?.();
|
|
388
427
|
this.releaseCaret = undefined;
|
|
389
428
|
this.sync();
|
|
429
|
+
const parsed = this.numeric;
|
|
430
|
+
this.toggleState('at-min', parsed !== null && this.min !== undefined && parsed <= this.min);
|
|
431
|
+
this.toggleState('at-max', parsed !== null && this.max !== undefined && parsed >= this.max);
|
|
390
432
|
}
|
|
391
433
|
get blocksImplicitSubmission() {
|
|
392
434
|
return true;
|
|
@@ -419,6 +461,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
419
461
|
?disabled=${this.isDisabled || this.readOnly || atMin}
|
|
420
462
|
@click=${this.handleStepperClick}
|
|
421
463
|
@lostpointercapture=${this.stopRepeat}
|
|
464
|
+
@mousedown=${this.handleStepperMouseDown}
|
|
422
465
|
@pointercancel=${this.stopRepeat}
|
|
423
466
|
@pointerdown=${this.handleStepperDown}
|
|
424
467
|
@pointerup=${this.stopRepeat}
|
|
@@ -434,7 +477,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
434
477
|
type="text"
|
|
435
478
|
role=${this.touchApple ? nothing : 'spinbutton'}
|
|
436
479
|
aria-roledescription=${this.touchApple || this.roleLabel === '' ? nothing : this.roleLabel}
|
|
437
|
-
inputmode
|
|
480
|
+
inputmode=${this.keypad}
|
|
438
481
|
autocomplete=${this.autocomplete || nothing}
|
|
439
482
|
name=${this.name || nothing}
|
|
440
483
|
placeholder=${this.placeholder || nothing}
|
|
@@ -474,6 +517,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
474
517
|
?disabled=${this.isDisabled || this.readOnly || atMax}
|
|
475
518
|
@click=${this.handleStepperClick}
|
|
476
519
|
@lostpointercapture=${this.stopRepeat}
|
|
520
|
+
@mousedown=${this.handleStepperMouseDown}
|
|
477
521
|
@pointercancel=${this.stopRepeat}
|
|
478
522
|
@pointerdown=${this.handleStepperDown}
|
|
479
523
|
@pointerup=${this.stopRepeat}
|
|
@@ -542,7 +586,7 @@ __decorate([
|
|
|
542
586
|
property({ reflect: true, type: Boolean })
|
|
543
587
|
], HelixHeadlessNumberInput.prototype, "scrub", void 0);
|
|
544
588
|
__decorate([
|
|
545
|
-
property({
|
|
589
|
+
property({ converter: stepConverter })
|
|
546
590
|
], HelixHeadlessNumberInput.prototype, "step", void 0);
|
|
547
591
|
__decorate([
|
|
548
592
|
words,
|
|
@@ -99,6 +99,8 @@ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEnt
|
|
|
99
99
|
* @aliases JSON viewer, Object viewer, Inspector, Data explorer
|
|
100
100
|
* @documentation https://fusion.dev/helix/components/object-inspector
|
|
101
101
|
*
|
|
102
|
+
* @slot empty - Shown in the tree while it has no entries to show. Defaults to `empty-label`, or `no-matches-label` while a filter matched none.
|
|
103
|
+
*
|
|
102
104
|
* @event {HelixActivateEvent} helix-activate - The active row moved, by a key or a click. The detail's `value` is its path.
|
|
103
105
|
* @event {HelixBeforeActivateEvent} helix-before-activate - The active row is about to move, with its new path. Cancelable: cancel it to keep the row where it is.
|
|
104
106
|
* @event {HelixBeforeCloseEvent} helix-before-close - A reader is about to close the active branch. Cancelable: cancel it to keep the branch open.
|
|
@@ -128,6 +130,7 @@ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEnt
|
|
|
128
130
|
* @csspart copy-tooltip-right - The tooltip while it is right of the copy button. Set alongside `copy-tooltip`.
|
|
129
131
|
* @csspart copy-tooltip-top - The tooltip while it is above the copy button. Set alongside `copy-tooltip`.
|
|
130
132
|
* @csspart count - The number of entries a branch holds.
|
|
133
|
+
* @csspart empty - The row shown in the tree while it has no entries to show: no value, or a filter that matched none. It holds the `empty` slot.
|
|
131
134
|
* @csspart frame - The tree and the copy button beside it.
|
|
132
135
|
* @csspart header - The bar that holds the label, when there is one.
|
|
133
136
|
* @csspart key - A property name or an array index.
|
|
@@ -146,6 +149,10 @@ export type ObjectInspectorRenderer = (value: unknown, entry: ObjectInspectorEnt
|
|
|
146
149
|
* @csspart value - What `renderValue` drew for an entry, in place of its own drawing.
|
|
147
150
|
*
|
|
148
151
|
* @cssproperty --object-inspector-level - How deep a row is, from 0 at the root, for indenting it.
|
|
152
|
+
*
|
|
153
|
+
* @cssstate empty - While the tree has no entries to show: no value, or a filter that matched none.
|
|
154
|
+
* @cssstate filtered - While `filter` holds text.
|
|
155
|
+
* @cssstate matched - While `filter` holds text and matched at least one entry.
|
|
149
156
|
*/
|
|
150
157
|
export declare class HelixHeadlessObjectInspector extends HelixElement {
|
|
151
158
|
/**
|
|
@@ -183,6 +190,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
|
|
|
183
190
|
* wall of text this exists to avoid.
|
|
184
191
|
*/
|
|
185
192
|
defaultDepth: number;
|
|
193
|
+
/**
|
|
194
|
+
* What the tree says while there is no value to show, such as before a
|
|
195
|
+
* fetch answers. The `empty` slot replaces it.
|
|
196
|
+
*/
|
|
197
|
+
emptyLabel: string;
|
|
186
198
|
/**
|
|
187
199
|
* The open branches, by path, in no set order.
|
|
188
200
|
*
|
|
@@ -254,6 +266,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
|
|
|
254
266
|
* reach a value.
|
|
255
267
|
*/
|
|
256
268
|
noCopy: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* What the tree says while a filter matched no entry. The `empty` slot
|
|
271
|
+
* replaces it.
|
|
272
|
+
*/
|
|
273
|
+
noMatchesLabel: string;
|
|
257
274
|
/**
|
|
258
275
|
* Draws an entry's value your way, given the value and its entry. Set it as
|
|
259
276
|
* a property.
|
|
@@ -358,6 +375,12 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
|
|
|
358
375
|
private seed;
|
|
359
376
|
/** Opens the branches around each path, where the value has them. */
|
|
360
377
|
private openAround;
|
|
378
|
+
/**
|
|
379
|
+
* Whether the tree has no entries to show. Not while a value named by `for`
|
|
380
|
+
* is still to be read, as on a server and while hydrating what one sent,
|
|
381
|
+
* where it says nothing yet about the value.
|
|
382
|
+
*/
|
|
383
|
+
private get blank();
|
|
361
384
|
protected willUpdate(changed: PropertyValues): void;
|
|
362
385
|
/**
|
|
363
386
|
* A count in its plural message, `items-label`, `keys-label` or
|
|
@@ -32,6 +32,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
|
|
|
32
32
|
this.copyLabel = 'Copy {name}';
|
|
33
33
|
this.copyValueLabel = 'Copy value';
|
|
34
34
|
this.defaultDepth = 1;
|
|
35
|
+
this.emptyLabel = 'Nothing to show';
|
|
35
36
|
this.filter = '';
|
|
36
37
|
this.filterable = false;
|
|
37
38
|
this.filterLabel = 'Filter';
|
|
@@ -40,6 +41,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
|
|
|
40
41
|
this.keysLabel = '{count, plural, one {# key} other {# keys}}';
|
|
41
42
|
this.label = '';
|
|
42
43
|
this.noCopy = false;
|
|
44
|
+
this.noMatchesLabel = 'No matches';
|
|
43
45
|
this.resultsLabel = '{count, plural, one {# result} other {# results}}';
|
|
44
46
|
this.treeLabel = 'Value';
|
|
45
47
|
this.value = undefined;
|
|
@@ -213,6 +215,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
|
|
|
213
215
|
openAround(paths) {
|
|
214
216
|
this.opened = withAncestors(this.opened, this.branches, paths);
|
|
215
217
|
}
|
|
218
|
+
get blank() {
|
|
219
|
+
return this.rows.length === 0 && (this.for === '' || this.loadedFor === this.for);
|
|
220
|
+
}
|
|
216
221
|
willUpdate(changed) {
|
|
217
222
|
super.willUpdate(changed);
|
|
218
223
|
this.hadFocus = this.hasFocusedRow();
|
|
@@ -248,6 +253,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
|
|
|
248
253
|
if (changed.has('filter') && this.hasUpdated) {
|
|
249
254
|
this.tell();
|
|
250
255
|
}
|
|
256
|
+
this.toggleState('empty', this.blank);
|
|
257
|
+
this.toggleState('filtered', this.found !== undefined);
|
|
258
|
+
this.toggleState('matched', (this.found?.size ?? 0) > 0);
|
|
251
259
|
}
|
|
252
260
|
counted(which, count) {
|
|
253
261
|
const one = this[`${which}Label`];
|
|
@@ -526,7 +534,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
|
|
|
526
534
|
@scroll=${() => this.virtualize.measure()}
|
|
527
535
|
>
|
|
528
536
|
${this.windowed ? html `<div part="before" role="presentation" style="block-size: ${before}px"></div>` : nothing}
|
|
529
|
-
${repeat(shown, ([row]) => row.path, ([row, at]) => this.renderRow(row, at))}
|
|
537
|
+
${repeat(shown, ([row]) => row.path, ([row, at]) => this.renderRow(row, at))}${this.blank
|
|
538
|
+
? html `<div part="empty" role="row"><div role="gridcell"><slot name="empty">${this.found ? this.noMatchesLabel : this.emptyLabel}</slot></div></div>`
|
|
539
|
+
: nothing}
|
|
530
540
|
${this.windowed ? html `<div part="after" role="presentation" style="block-size: ${after}px"></div>` : nothing}
|
|
531
541
|
</div>
|
|
532
542
|
${this.noCopy ? nothing : this.renderCopyButton(rows[this.cursor])}
|
|
@@ -548,6 +558,10 @@ __decorate([
|
|
|
548
558
|
__decorate([
|
|
549
559
|
property({ attribute: 'default-depth', type: Number })
|
|
550
560
|
], HelixHeadlessObjectInspector.prototype, "defaultDepth", void 0);
|
|
561
|
+
__decorate([
|
|
562
|
+
words,
|
|
563
|
+
property({ attribute: 'empty-label' })
|
|
564
|
+
], HelixHeadlessObjectInspector.prototype, "emptyLabel", void 0);
|
|
551
565
|
__decorate([
|
|
552
566
|
property({ attribute: false })
|
|
553
567
|
], HelixHeadlessObjectInspector.prototype, "expanded", null);
|
|
@@ -584,6 +598,10 @@ __decorate([
|
|
|
584
598
|
__decorate([
|
|
585
599
|
property({ attribute: 'no-copy', type: Boolean })
|
|
586
600
|
], HelixHeadlessObjectInspector.prototype, "noCopy", void 0);
|
|
601
|
+
__decorate([
|
|
602
|
+
words,
|
|
603
|
+
property({ attribute: 'no-matches-label' })
|
|
604
|
+
], HelixHeadlessObjectInspector.prototype, "noMatchesLabel", void 0);
|
|
587
605
|
__decorate([
|
|
588
606
|
property({ attribute: false })
|
|
589
607
|
], HelixHeadlessObjectInspector.prototype, "renderValue", void 0);
|