@fusion.dev/helix-headless 0.174.0 → 0.176.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 +198 -59
- package/dist/app-header/root/app-header-root.d.ts +2 -1
- package/dist/app-header/root/app-header-root.js +6 -1
- package/dist/button/root/button-root.d.ts +6 -1
- package/dist/button/root/button-root.js +1 -0
- package/dist/command/root/command-root.d.ts +4 -2
- package/dist/command/root/command-root.js +1 -0
- package/dist/date-picker/root/date-picker-root.d.ts +6 -0
- package/dist/date-picker/root/date-picker-root.js +7 -1
- package/dist/dialog/form-method.d.ts +12 -0
- package/dist/dialog/form-method.js +13 -0
- package/dist/dialog/root/dialog-root.d.ts +1 -9
- package/dist/dialog/root/dialog-root.js +2 -11
- package/dist/drawer/root/drawer-root.d.ts +2 -0
- package/dist/drawer/root/drawer-root.js +6 -0
- package/dist/filter-input/index.d.ts +0 -1
- package/dist/filter-input/index.js +0 -1
- package/dist/filter-input/menu/filter-menu.d.ts +4 -0
- package/dist/filter-input/menu/index.d.ts +1 -0
- package/dist/filter-input/menu/index.js +1 -0
- package/dist/filter-input/root/filter-input-root.d.ts +26 -2
- package/dist/filter-input/root/filter-input-root.js +39 -5
- package/dist/icon/all/index.d.ts +1 -0
- package/dist/icon/all/index.js +4 -0
- package/dist/icon/root/icon-root.d.ts +9 -8
- package/dist/icon/root/icon-root.js +4 -2
- package/dist/icon/set.js +16 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lightbox/item/lightbox-item.d.ts +1 -0
- package/dist/lightbox/root/lightbox-root.d.ts +25 -5
- package/dist/lightbox/root/lightbox-root.js +33 -10
- package/dist/notifications/group/notification-group.js +2 -2
- package/dist/notifications/notification/notification.js +4 -5
- package/dist/notifications/root/notifications-root.d.ts +4 -3
- package/dist/notifications/root/notifications-root.js +1 -1
- package/dist/pagination/root/pagination-root.d.ts +2 -1
- package/dist/pagination/root/pagination-root.js +6 -1
- package/dist/tooltip/root/tooltip-root.d.ts +5 -2
- package/dist/tooltip/root/tooltip-root.js +11 -11
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +102 -72
- package/types/vue.d.ts +13 -14
|
@@ -348,6 +348,12 @@ export declare class HelixHeadlessDatePicker extends HelixHeadlessDatePicker_bas
|
|
|
348
348
|
* to the first of the month. Measured before today was a candidate: a
|
|
349
349
|
* calendar with no value opened on October 5 with its stop on "Thursday,
|
|
350
350
|
* October 1".
|
|
351
|
+
*
|
|
352
|
+
* Today counts only where it can be chosen, and the fallback is the first
|
|
353
|
+
* day of the month that can, so a reader tabbing in lands on a date they can
|
|
354
|
+
* pick. Measured before: with `min="2026-03-03"`, the stop sat on "Sunday,
|
|
355
|
+
* March 1, 2026", unavailable. A month with nothing to choose keeps its
|
|
356
|
+
* first day, so the grid still has a way in.
|
|
351
357
|
*/
|
|
352
358
|
private get anchor();
|
|
353
359
|
/** Six weeks of dates, so a grid does not change height between months. */
|
|
@@ -199,15 +199,21 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinPickerF
|
|
|
199
199
|
}
|
|
200
200
|
get anchor() {
|
|
201
201
|
const shown = this.shown;
|
|
202
|
+
const today = asDate(todayIn(this.timeZone));
|
|
202
203
|
for (const candidate of [
|
|
203
204
|
asDate(this.focused),
|
|
204
205
|
asDate(this.selected[0] ?? ''),
|
|
205
|
-
|
|
206
|
+
today && !this.barred(today) ? today : null,
|
|
206
207
|
]) {
|
|
207
208
|
if (candidate && this.system.same(candidate, shown)) {
|
|
208
209
|
return candidate;
|
|
209
210
|
}
|
|
210
211
|
}
|
|
212
|
+
for (let day = shown; this.system.same(day, shown); day = addDays(day, 1)) {
|
|
213
|
+
if (!this.barred(day)) {
|
|
214
|
+
return day;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
211
217
|
return shown;
|
|
212
218
|
}
|
|
213
219
|
weeksOf(month) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closes a shadow `<dialog>` on a `<form method="dialog">` submit slotted into
|
|
3
|
+
* it, the way the platform closes its own. `Dialog` and `Drawer` share it.
|
|
4
|
+
*
|
|
5
|
+
* **The platform cannot find the dialog from the form.** A submit with the
|
|
6
|
+
* `dialog` method closes the nearest `<dialog>` ancestor, and a slotted form's
|
|
7
|
+
* ancestors are the light DOM, where there is none. So the submit is read on
|
|
8
|
+
* the host, and the submitter's `value` is the `returnValue`, as it would be.
|
|
9
|
+
* A form inside a `<dialog>` of its own, or with any other method, is left to
|
|
10
|
+
* the platform.
|
|
11
|
+
*/
|
|
12
|
+
export declare const closeOnDialogSubmit: (event: SubmitEvent, close: (returnValue?: string) => void) => void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const closeOnDialogSubmit = (event, close) => {
|
|
2
|
+
const form = event.target;
|
|
3
|
+
if (!(form instanceof HTMLFormElement) || form.closest('dialog')) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const submitter = event.submitter;
|
|
7
|
+
const method = submitter?.hasAttribute('formmethod') ? submitter.formMethod : form.method;
|
|
8
|
+
if (method !== 'dialog') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
close(submitter?.hasAttribute('value') ? submitter.value : undefined);
|
|
13
|
+
};
|
|
@@ -201,15 +201,7 @@ export declare class HelixHeadlessDialog extends HelixHeadlessDialog_base {
|
|
|
201
201
|
* saying otherwise would be a lie the next `showModal()` would trip over.
|
|
202
202
|
*/
|
|
203
203
|
private readonly handleClose;
|
|
204
|
-
/**
|
|
205
|
-
* A `<form method="dialog">` slotted into the dialog, closed the way the
|
|
206
|
-
* platform closes its own.
|
|
207
|
-
*
|
|
208
|
-
* **The platform cannot find this dialog from the form.** A submit with the
|
|
209
|
-
* `dialog` method closes the nearest `<dialog>` ancestor, and a slotted form's
|
|
210
|
-
* ancestors are the light DOM, where there is none. So the submit is read
|
|
211
|
-
* here, and the submitter's `value` is the `returnValue`, as it would be.
|
|
212
|
-
*/
|
|
204
|
+
/** A `<form method="dialog">` slotted into the dialog closes it. */
|
|
213
205
|
private readonly handleSubmit;
|
|
214
206
|
/** Whether the `header` slot's heading is what currently names the dialog. */
|
|
215
207
|
private namedByHeader;
|
|
@@ -8,6 +8,7 @@ import { AriaForwardController, auditName, CommandController, customElement, dra
|
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { wordsIn } from '../description.js';
|
|
11
|
+
import { closeOnDialogSubmit } from '../form-method.js';
|
|
11
12
|
let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(HelixElement) {
|
|
12
13
|
constructor() {
|
|
13
14
|
super(...arguments);
|
|
@@ -49,17 +50,7 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
|
|
|
49
50
|
this.relayNativeEvent(event);
|
|
50
51
|
};
|
|
51
52
|
this.handleSubmit = (event) => {
|
|
52
|
-
|
|
53
|
-
if (!(form instanceof HTMLFormElement) || form.closest('dialog')) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const submitter = event.submitter;
|
|
57
|
-
const method = submitter?.hasAttribute('formmethod') ? submitter.formMethod : form.method;
|
|
58
|
-
if (method !== 'dialog') {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
event.preventDefault();
|
|
62
|
-
this.close(submitter?.hasAttribute('value') ? submitter.value : undefined);
|
|
53
|
+
closeOnDialogSubmit(event, (value) => this.close(value));
|
|
63
54
|
};
|
|
64
55
|
this.namedByHeader = false;
|
|
65
56
|
this.scope = new FocusScopeController(this, {
|
|
@@ -321,6 +321,8 @@ export declare class HelixHeadlessDrawer extends HelixHeadlessDrawer_base {
|
|
|
321
321
|
* comes first.
|
|
322
322
|
*/
|
|
323
323
|
private describeByBody;
|
|
324
|
+
/** A `<form method="dialog">` slotted into the drawer closes it, as in `Dialog`. */
|
|
325
|
+
private readonly handleSubmit;
|
|
324
326
|
connectedCallback(): void;
|
|
325
327
|
disconnectedCallback(): void;
|
|
326
328
|
protected updated(changed: PropertyValues): void;
|
|
@@ -8,6 +8,7 @@ import { AriaForwardController, auditName, CommandController, customElement, dra
|
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { wordsIn } from '../../dialog/description.js';
|
|
11
|
+
import { closeOnDialogSubmit } from '../../dialog/form-method.js';
|
|
11
12
|
import { beyondWindow } from '../reveal.js';
|
|
12
13
|
import { startsSwipe } from '../swipe-start.js';
|
|
13
14
|
let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(HelixElement) {
|
|
@@ -111,6 +112,9 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
|
|
|
111
112
|
};
|
|
112
113
|
this.namedByHeader = false;
|
|
113
114
|
this.describedByBody = false;
|
|
115
|
+
this.handleSubmit = (event) => {
|
|
116
|
+
closeOnDialogSubmit(event, (value) => this.close(value));
|
|
117
|
+
};
|
|
114
118
|
}
|
|
115
119
|
static { this.words = 'helix-drawer'; }
|
|
116
120
|
get refusesDismissal() {
|
|
@@ -214,10 +218,12 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
|
|
|
214
218
|
connectedCallback() {
|
|
215
219
|
super.connectedCallback();
|
|
216
220
|
this.addEventListener('focusin', this.handleFocusIn);
|
|
221
|
+
this.addEventListener('submit', this.handleSubmit);
|
|
217
222
|
}
|
|
218
223
|
disconnectedCallback() {
|
|
219
224
|
super.disconnectedCallback();
|
|
220
225
|
this.removeEventListener('focusin', this.handleFocusIn);
|
|
226
|
+
this.removeEventListener('submit', this.handleSubmit);
|
|
221
227
|
unlockScroll(this);
|
|
222
228
|
}
|
|
223
229
|
updated(changed) {
|
|
@@ -51,6 +51,10 @@ import '../../menu/index.js';
|
|
|
51
51
|
* `Notification` does for a read. Call `preventDefault()` on `helix-facet` to
|
|
52
52
|
* cancel the change.
|
|
53
53
|
*
|
|
54
|
+
* Imported from its own entry, `@fusion.dev/helix-headless/filter-input/menu`, which registers
|
|
55
|
+
* it with the `Menu` it draws its pages with. The family's entry leaves it out,
|
|
56
|
+
* so a field without a picker does not carry `Menu`.
|
|
57
|
+
*
|
|
54
58
|
* @since 0.25.0
|
|
55
59
|
* @status stable
|
|
56
60
|
* @category forms
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './filter-menu.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './filter-menu.js';
|
|
@@ -42,7 +42,7 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
42
42
|
* | Enter | Presses the highlighted suggestion. With none highlighted, reads the typed filters into chips, or submits the form when there are none. |
|
|
43
43
|
* | Down, Up | Moves the highlight through the suggestions, wrapping, and opens the panel. |
|
|
44
44
|
* | Home, End | Highlights the first or the last suggestion while the panel is open. |
|
|
45
|
-
* | Escape | Closes the panel and stops there, so a dialog around the field stays open. |
|
|
45
|
+
* | Escape | Closes the panel and stops there, so a dialog around the field stays open. With the panel closed, empties the text. In an empty field, does nothing, so the page hears it. |
|
|
46
46
|
* | Backspace | In an empty field, asks the last token to go. |
|
|
47
47
|
* | Left | With the caret at the start of the text, moves to the last token's remove button. Right on a right-to-left page. |
|
|
48
48
|
* | Left, Right on a token | Move between the tokens, and past the last back to the text. |
|
|
@@ -51,7 +51,8 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
51
51
|
* **Typed filters.** The field parses only the fields you declare. Write
|
|
52
52
|
* `FilterField` elements, usually inside the `FilterMenu` slotted into `start`,
|
|
53
53
|
* so the picker and the text share one set. On Enter, the field reads the text
|
|
54
|
-
* against them
|
|
54
|
+
* against them, and a paste is read the same way as soon as it lands.
|
|
55
|
+
* `disposition:refunded,settled` and `-disposition:refunded` fire
|
|
55
56
|
* `helix-facet` exactly as ticking those values in the picker would, and the
|
|
56
57
|
* parsed text is removed. Everything else stays, so `refund tuesday
|
|
57
58
|
* disposition:settled` is a filter and a search at once. Helix defines the
|
|
@@ -64,6 +65,12 @@ declare const HelixHeadlessFilterInput_base: (new (...args: any[]) => import("@f
|
|
|
64
65
|
* nothing draws it and the menu has no page for it, and the grammar still reads
|
|
65
66
|
* it.
|
|
66
67
|
*
|
|
68
|
+
* **The picker has an entry of its own.** `FilterMenu` draws its pages with
|
|
69
|
+
* `Menu`, which a field without a picker does not need, so this family's entry
|
|
70
|
+
* leaves it out. A page that writes one imports
|
|
71
|
+
* `@fusion.dev/helix/filter-input/menu` as well, and a picker written without it
|
|
72
|
+
* is reported once the page has loaded, with the entry to add.
|
|
73
|
+
*
|
|
67
74
|
* **Suggestions.** Slot options into `suggestions` and the field shows them,
|
|
68
75
|
* moves through them with the arrow keys, and presses one on Enter. The option
|
|
69
76
|
* fires its own `helix-select`, and you decide what choosing it means. Keys
|
|
@@ -408,6 +415,14 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
408
415
|
*
|
|
409
416
|
* Everything else in the row is untouched: a plain `Tag`, or a chip for
|
|
410
417
|
* something no `FilterField` declares, is the consumer's to answer.
|
|
418
|
+
*
|
|
419
|
+
* **Heard on the way down, in the capture phase**, because the chip is a
|
|
420
|
+
* child of this element and its request passes here before it reaches the
|
|
421
|
+
* chip's listeners or comes back up. Stopped there, no listener on the chip,
|
|
422
|
+
* on this element or above it hears it. Heard on the way up, `stopPropagation`
|
|
423
|
+
* kept it from the ancestors and not from this element's other listeners, so a
|
|
424
|
+
* consumer listening on the field heard `helix-dismiss` as well as the
|
|
425
|
+
* `helix-facet` that answered it.
|
|
411
426
|
*/
|
|
412
427
|
private readonly handleDismiss;
|
|
413
428
|
/**
|
|
@@ -512,6 +527,15 @@ export declare class HelixHeadlessFilterInput extends HelixHeadlessFilterInput_b
|
|
|
512
527
|
disconnectedCallback(): void;
|
|
513
528
|
protected willUpdate(changed: PropertyValues): void;
|
|
514
529
|
protected firstUpdated(changed: PropertyValues): void;
|
|
530
|
+
/**
|
|
531
|
+
* Says so when a picker is written in the field and never registered.
|
|
532
|
+
*
|
|
533
|
+
* The picker has an entry of its own, so a page that imports only the field
|
|
534
|
+
* gets an inert element there: a trigger that opens nothing. Asked once the
|
|
535
|
+
* page has loaded and painted, so an entry a later module imports is not
|
|
536
|
+
* taken for a missing one.
|
|
537
|
+
*/
|
|
538
|
+
private auditPicker;
|
|
515
539
|
protected updated(changed: PropertyValues): void;
|
|
516
540
|
/**
|
|
517
541
|
* Marks the highlighted suggestion, and tells the control which one it is.
|
|
@@ -4,14 +4,15 @@ 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, countLabel, customElement, drawIcon, drawStatus, formatWords, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, StatusController, scrollItemIntoView, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, countLabel, customElement, drawIcon, drawStatus, formatWords, HelixFacetEvent, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, reportRuntime, restoreValue, StatusController, scrollItemIntoView, words, wordsLocale, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { x } from '@fusion.dev/helix-icons';
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { HelixHeadlessFilterField } from '../field/filter-field.js';
|
|
12
12
|
import { readFilterText } from '../grammar.js';
|
|
13
|
-
import { HelixHeadlessFilterMenu } from '../menu/filter-menu.js';
|
|
14
13
|
import { currentTerm, suggests, tokenEntry } from '../suggest.js';
|
|
14
|
+
const picks = (element) => typeof element.apply === 'function' &&
|
|
15
|
+
Array.isArray(element.fields);
|
|
15
16
|
const asksToGo = (node) => typeof node?.dismiss === 'function';
|
|
16
17
|
const isOption = (node) => 'active' in node && 'value' in node;
|
|
17
18
|
let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
|
|
@@ -58,6 +59,9 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
58
59
|
restoreValue(control, this.value);
|
|
59
60
|
this.dismissed = false;
|
|
60
61
|
this.settleOpen();
|
|
62
|
+
if (event.inputType === 'insertFromPaste') {
|
|
63
|
+
this.readFilters();
|
|
64
|
+
}
|
|
61
65
|
};
|
|
62
66
|
this.handleChange = (event) => {
|
|
63
67
|
event.stopPropagation();
|
|
@@ -111,6 +115,12 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
111
115
|
}
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
118
|
+
if (event.key === 'Escape' && !this.open && this.value !== '') {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
event.stopPropagation();
|
|
121
|
+
this.handleClear();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
114
124
|
if (!this.suggesting) {
|
|
115
125
|
return;
|
|
116
126
|
}
|
|
@@ -281,7 +291,7 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
281
291
|
}
|
|
282
292
|
menuFor(field) {
|
|
283
293
|
for (const menu of this.querySelectorAll('*')) {
|
|
284
|
-
if (!(menu
|
|
294
|
+
if (!picks(menu)) {
|
|
285
295
|
continue;
|
|
286
296
|
}
|
|
287
297
|
if (menu.fields.some((declared) => (declared.name ?? '') === field)) {
|
|
@@ -387,12 +397,12 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
387
397
|
}
|
|
388
398
|
connectedCallback() {
|
|
389
399
|
super.connectedCallback();
|
|
390
|
-
this.addEventListener('helix-dismiss', this.handleDismiss);
|
|
400
|
+
this.addEventListener('helix-dismiss', this.handleDismiss, { capture: true });
|
|
391
401
|
this.addEventListener(HelixFacetEvent.eventName, this.handleFacet);
|
|
392
402
|
}
|
|
393
403
|
disconnectedCallback() {
|
|
394
404
|
super.disconnectedCallback();
|
|
395
|
-
this.removeEventListener('helix-dismiss', this.handleDismiss);
|
|
405
|
+
this.removeEventListener('helix-dismiss', this.handleDismiss, { capture: true });
|
|
396
406
|
this.removeEventListener(HelixFacetEvent.eventName, this.handleFacet);
|
|
397
407
|
}
|
|
398
408
|
willUpdate(changed) {
|
|
@@ -418,6 +428,30 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinContr
|
|
|
418
428
|
this.finishHydration();
|
|
419
429
|
this.handleTokens();
|
|
420
430
|
this.refreshSuggesting();
|
|
431
|
+
this.auditPicker();
|
|
432
|
+
}
|
|
433
|
+
auditPicker() {
|
|
434
|
+
const { tier } = this.constructor;
|
|
435
|
+
const tag = `${tier}filter-menu`;
|
|
436
|
+
const check = () => {
|
|
437
|
+
if (this.isConnected && this.querySelector(tag) && !customElements.get(tag)) {
|
|
438
|
+
const entry = tier === 'helix-' ? '@fusion.dev/helix' : '@fusion.dev/helix-headless';
|
|
439
|
+
reportRuntime({
|
|
440
|
+
kind: 'unregistered-part',
|
|
441
|
+
message: `<${tag}> in <${this.localName}> is not registered, so its trigger opens nothing. Import '${entry}/filter-input/menu'.`,
|
|
442
|
+
tag: this.localName,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const frame = () => {
|
|
447
|
+
requestAnimationFrame(check);
|
|
448
|
+
};
|
|
449
|
+
if (document.readyState === 'complete') {
|
|
450
|
+
frame();
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
window.addEventListener('load', frame, { once: true });
|
|
454
|
+
}
|
|
421
455
|
}
|
|
422
456
|
updated(changed) {
|
|
423
457
|
super.updated(changed);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../index.js';
|
|
@@ -18,8 +18,11 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
|
18
18
|
* a dark background. Shapes that set their own `fill` or `stroke` are
|
|
19
19
|
* unaffected.
|
|
20
20
|
*
|
|
21
|
-
* **A name is looked up in a registry.** The family's entry registers
|
|
22
|
-
*
|
|
21
|
+
* **A name is looked up in a registry.** The family's entry registers a small
|
|
22
|
+
* set: the four carets, `arrow-left`, `arrow-right`, `check`, `x`, `plus`,
|
|
23
|
+
* `minus`, `magnifying-glass`, `info` and `warning`. `icon/all` registers the
|
|
24
|
+
* whole set, so any name in it draws, and a name that resolves to nothing
|
|
25
|
+
* says to import it. The element's own entry,
|
|
23
26
|
* `@fusion.dev/helix-headless/icon/root`, registers none: a page that imports
|
|
24
27
|
* it and calls `registerIcons` with the glyphs it names ships those and no
|
|
25
28
|
* others. `registerIconLibrary` resolves names under a prefix of your own,
|
|
@@ -94,12 +97,10 @@ export declare class HelixHeadlessIcon extends HelixElement {
|
|
|
94
97
|
* The name of the icon in the Helix set.
|
|
95
98
|
*
|
|
96
99
|
* **A lookup, and the reason both this and `icon` exist.** A name is a string,
|
|
97
|
-
* so it is what markup can carry, and resolving one means an index
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* element from `icon/root` and registers the glyphs it names, or hands over
|
|
102
|
-
* the value instead.
|
|
100
|
+
* so it is what markup can carry, and resolving one means an index that no
|
|
101
|
+
* bundler can shake. The family's entry indexes a small set, `icon/all`
|
|
102
|
+
* indexes the whole set at 15 kB, and `icon/root` indexes nothing, for a
|
|
103
|
+
* page that registers the glyphs it names or hands over the value instead.
|
|
103
104
|
*
|
|
104
105
|
* A name written `prefix:name` asks the library registered under that
|
|
105
106
|
* prefix.
|
|
@@ -131,8 +131,10 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
131
131
|
kind: 'unknown-icon',
|
|
132
132
|
message: this.src === ''
|
|
133
133
|
? `<${this.localName} name="${asked}"> names no registered icon, so it draws ` +
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
`nothing. For a glyph of the Helix set, import '${this.localName.startsWith('helix-headless-')
|
|
135
|
+
? '@fusion.dev/helix-headless'
|
|
136
|
+
: '@fusion.dev/helix'}/icon/all', which registers every one. Otherwise check the name, or register ` +
|
|
137
|
+
'the glyph with `registerIcons`.'
|
|
136
138
|
: `<${this.localName} src="${asked}"> could not be fetched as an SVG file, so it ` +
|
|
137
139
|
'draws nothing. Check the URL, and that the file is an `<svg>`.',
|
|
138
140
|
tag: this.localName,
|
package/dist/icon/set.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { arrowLeft, arrowRight, caretDown, caretLeft, caretRight, caretUp, check, info, magnifyingGlass, minus, plus, warning, x, } from '@fusion.dev/helix-icons';
|
|
2
2
|
import { registerIcons } from './registry.js';
|
|
3
|
-
registerIcons(
|
|
3
|
+
registerIcons([
|
|
4
|
+
arrowLeft,
|
|
5
|
+
arrowRight,
|
|
6
|
+
caretDown,
|
|
7
|
+
caretLeft,
|
|
8
|
+
caretRight,
|
|
9
|
+
caretUp,
|
|
10
|
+
check,
|
|
11
|
+
info,
|
|
12
|
+
magnifyingGlass,
|
|
13
|
+
minus,
|
|
14
|
+
plus,
|
|
15
|
+
warning,
|
|
16
|
+
x,
|
|
17
|
+
]);
|
package/dist/index.d.ts
CHANGED
|
@@ -46,10 +46,11 @@ export * from './drilldown/index.js';
|
|
|
46
46
|
export * from './empty/index.js';
|
|
47
47
|
export * from './file-input/index.js';
|
|
48
48
|
export * from './filter-input/index.js';
|
|
49
|
+
export * from './filter-input/menu/index.js';
|
|
49
50
|
export * from './format-date/index.js';
|
|
50
51
|
export * from './graph/index.js';
|
|
51
52
|
export * from './heatmap/index.js';
|
|
52
|
-
export * from './icon/index.js';
|
|
53
|
+
export * from './icon/all/index.js';
|
|
53
54
|
export * from './icon-button/index.js';
|
|
54
55
|
export * from './image/index.js';
|
|
55
56
|
export * from './input/index.js';
|
package/dist/index.js
CHANGED
|
@@ -37,10 +37,11 @@ export * from './drilldown/index.js';
|
|
|
37
37
|
export * from './empty/index.js';
|
|
38
38
|
export * from './file-input/index.js';
|
|
39
39
|
export * from './filter-input/index.js';
|
|
40
|
+
export * from './filter-input/menu/index.js';
|
|
40
41
|
export * from './format-date/index.js';
|
|
41
42
|
export * from './graph/index.js';
|
|
42
43
|
export * from './heatmap/index.js';
|
|
43
|
-
export * from './icon/index.js';
|
|
44
|
+
export * from './icon/all/index.js';
|
|
44
45
|
export * from './icon-button/index.js';
|
|
45
46
|
export * from './image/index.js';
|
|
46
47
|
export * from './input/index.js';
|
|
@@ -55,6 +55,7 @@ export declare class HelixHeadlessLightboxItem extends HelixElement {
|
|
|
55
55
|
roleLabel: string;
|
|
56
56
|
/**
|
|
57
57
|
* The item's identifier. The root compares its `active` value against it.
|
|
58
|
+
* Without one, the item is named by its place in the set, counted from 1.
|
|
58
59
|
*/
|
|
59
60
|
value: string;
|
|
60
61
|
/** Whether anything is slotted into `description`, which then describes the group. */
|
|
@@ -16,7 +16,9 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
|
|
|
16
16
|
*
|
|
17
17
|
* A lightbox shows one item at a time and has no scrolling track, so `active`
|
|
18
18
|
* holds the current item, as in `Tabs`. The arrow keys change the item while
|
|
19
|
-
* focus stays in the dialog.
|
|
19
|
+
* focus stays in the dialog. `active` names an item by its `value`, and an
|
|
20
|
+
* item with no `value` by its place, counted from 1, as in `Steps`, so a set
|
|
21
|
+
* written without values moves and `helix-activate` says `1`, `2` and so on.
|
|
20
22
|
*
|
|
21
23
|
* Set `controls` for the viewer's own previous, next and close controls, named
|
|
22
24
|
* by `previous-label`, `next-label` and `dismiss-label`, whose glyphs you can
|
|
@@ -33,9 +35,9 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
|
|
|
33
35
|
* `--lightbox-pan-x` and `--lightbox-pan-y`, written only while zoomed, and a
|
|
34
36
|
* swipe moves to the next item only while the item is at its own size. With
|
|
35
37
|
* `controls`, the viewer also draws zoom-out and zoom-in controls, for a
|
|
36
|
-
* pointer with no wheel and no second finger. Once a reader stops zooming,
|
|
37
|
-
* live region says the new scale in
|
|
38
|
-
* "Zoomed to 150%".
|
|
38
|
+
* pointer with no wheel and no second finger. Once a reader stops zooming, and
|
|
39
|
+
* at once for a key or a zoom control, the live region says the new scale in
|
|
40
|
+
* the words of `zoom-status-label`, such as "Zoomed to 150%".
|
|
39
41
|
*
|
|
40
42
|
* Each item is a slide: a `group` with the role description in
|
|
41
43
|
* `slide-role-label`, named by its `label` or by `slide-label`. A touch or a pen
|
|
@@ -232,7 +234,14 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
232
234
|
private readonly zoomer;
|
|
233
235
|
/** Writes the zoom on the viewport, and nothing at the item's own size. */
|
|
234
236
|
private drawZoom;
|
|
235
|
-
/**
|
|
237
|
+
/** Whether the zoom is changing by one discrete step, a key or a control, which is said at once. */
|
|
238
|
+
private zoomStep;
|
|
239
|
+
/**
|
|
240
|
+
* Says the new scale in the live region: a moment after the last change of
|
|
241
|
+
* a pinch, a drag or the wheel, and at once for a key or a zoom control,
|
|
242
|
+
* which is one change a reader is waiting to hear. Said only after the
|
|
243
|
+
* pause, a reader pressing `+` heard nothing until it ended.
|
|
244
|
+
*/
|
|
236
245
|
private sayZoom;
|
|
237
246
|
/**
|
|
238
247
|
* Zooms the shown item to `scale`, held between `1` and `max-zoom`, about
|
|
@@ -346,12 +355,23 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
|
346
355
|
private zoomIn;
|
|
347
356
|
/** Zooms out by as much, as `-` and the zoom-out control do. */
|
|
348
357
|
private zoomOut;
|
|
358
|
+
/** Changes the zoom by one discrete step, which `sayZoom` says at once. */
|
|
359
|
+
private stepZoom;
|
|
349
360
|
/** Home and End, which are an index rather than a step. */
|
|
350
361
|
private toEnd;
|
|
351
362
|
/** The item shown at the last update, whose zoom a different item does not keep. */
|
|
352
363
|
private zoomedItem;
|
|
353
364
|
/** The item on screen: the one `active` names, or the first where it names none. */
|
|
354
365
|
private get shown();
|
|
366
|
+
/** The item `active` names, by `value` or by place, or the first where it names none. */
|
|
367
|
+
private named;
|
|
368
|
+
/**
|
|
369
|
+
* Requests a new shown item, and returns whether it moved.
|
|
370
|
+
*
|
|
371
|
+
* Where `active` names nothing, it is first written with the item on
|
|
372
|
+
* screen, so `helix-activate` names the item you saw as `previous`.
|
|
373
|
+
*/
|
|
374
|
+
requestActive(next: string, cause?: string): boolean;
|
|
355
375
|
/**
|
|
356
376
|
* A press on the backdrop or on the empty area around the item.
|
|
357
377
|
*
|
|
@@ -12,6 +12,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
12
12
|
import { HelixHeadlessLightboxItem } from '../item/lightbox-item.js';
|
|
13
13
|
import { percentOf } from '../zoom.js';
|
|
14
14
|
import { ZoomController } from '../zoom-controller.js';
|
|
15
|
+
const keyOf = (item, index) => item.value || String(index + 1);
|
|
15
16
|
let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerActive(MixinControllerOpen(HelixElement)) {
|
|
16
17
|
constructor() {
|
|
17
18
|
super(...arguments);
|
|
@@ -76,6 +77,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
76
77
|
onChange: (scale, pan) => this.drawZoom(scale, pan),
|
|
77
78
|
surface: () => this.partNamed('viewport'),
|
|
78
79
|
});
|
|
80
|
+
this.zoomStep = false;
|
|
79
81
|
this.fullscreen = new FullscreenController(this, (full) => {
|
|
80
82
|
this.filled = full;
|
|
81
83
|
});
|
|
@@ -164,7 +166,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
164
166
|
this.zoomSaid.cancel();
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
167
|
-
this.zoomSaid.start(400, () => {
|
|
169
|
+
this.zoomSaid.start(this.zoomStep ? 0 : 400, () => {
|
|
168
170
|
this.announcement = formatWords(this, 'zoomStatusLabel', {
|
|
169
171
|
percent: percentOf(this.zoomScale),
|
|
170
172
|
scale: this.zoomScale,
|
|
@@ -225,7 +227,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
225
227
|
: Math.min(Math.max(from + by, 0), last);
|
|
226
228
|
const target = items[next];
|
|
227
229
|
if (target && target !== shown) {
|
|
228
|
-
this.requestActive(target
|
|
230
|
+
this.requestActive(keyOf(target, next));
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
zoomKey(key) {
|
|
@@ -239,7 +241,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
239
241
|
this.zoomOut();
|
|
240
242
|
}
|
|
241
243
|
else if (key === '0') {
|
|
242
|
-
this.zoomer.reset();
|
|
244
|
+
this.stepZoom(() => this.zoomer.reset());
|
|
243
245
|
}
|
|
244
246
|
else {
|
|
245
247
|
return false;
|
|
@@ -266,19 +268,40 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
266
268
|
return true;
|
|
267
269
|
}
|
|
268
270
|
zoomIn() {
|
|
269
|
-
this.zoomer.zoomTo(this.zoomer.scale * 1.5);
|
|
271
|
+
this.stepZoom(() => this.zoomer.zoomTo(this.zoomer.scale * 1.5));
|
|
270
272
|
}
|
|
271
273
|
zoomOut() {
|
|
272
|
-
this.zoomer.zoomTo(this.zoomer.scale / 1.5);
|
|
274
|
+
this.stepZoom(() => this.zoomer.zoomTo(this.zoomer.scale / 1.5));
|
|
275
|
+
}
|
|
276
|
+
stepZoom(change) {
|
|
277
|
+
this.zoomStep = true;
|
|
278
|
+
try {
|
|
279
|
+
change();
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
this.zoomStep = false;
|
|
283
|
+
}
|
|
273
284
|
}
|
|
274
285
|
toEnd(at) {
|
|
275
286
|
const target = this.items[at];
|
|
276
287
|
if (target && target !== this.shown) {
|
|
277
|
-
this.requestActive(target
|
|
288
|
+
this.requestActive(keyOf(target, at));
|
|
278
289
|
}
|
|
279
290
|
}
|
|
280
291
|
get shown() {
|
|
281
|
-
return
|
|
292
|
+
return this.named(this.items);
|
|
293
|
+
}
|
|
294
|
+
named(items) {
|
|
295
|
+
const keyed = items.map((item, index) => ({ item, value: keyOf(item, index) }));
|
|
296
|
+
return resolveActiveItem(this.active ?? '', keyed)?.item;
|
|
297
|
+
}
|
|
298
|
+
requestActive(next, cause) {
|
|
299
|
+
const items = this.items;
|
|
300
|
+
const shown = this.active === '' ? this.shown : undefined;
|
|
301
|
+
if (shown) {
|
|
302
|
+
this.active = keyOf(shown, items.indexOf(shown));
|
|
303
|
+
}
|
|
304
|
+
return super.requestActive(next, cause);
|
|
282
305
|
}
|
|
283
306
|
dragged(event) {
|
|
284
307
|
const from = this.pressedAt;
|
|
@@ -348,7 +371,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
348
371
|
}
|
|
349
372
|
mark() {
|
|
350
373
|
const items = this.items;
|
|
351
|
-
const current =
|
|
374
|
+
const current = this.named(items);
|
|
352
375
|
items.forEach((item, at) => {
|
|
353
376
|
item.current = item === current;
|
|
354
377
|
whenHydrated(item, () => {
|
|
@@ -376,7 +399,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
376
399
|
return;
|
|
377
400
|
}
|
|
378
401
|
const items = this.items;
|
|
379
|
-
const current =
|
|
402
|
+
const current = this.named(items);
|
|
380
403
|
const at = current ? items.indexOf(current) : -1;
|
|
381
404
|
this.announcement =
|
|
382
405
|
at === -1 || !current
|
|
@@ -389,7 +412,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
|
|
|
389
412
|
}
|
|
390
413
|
place() {
|
|
391
414
|
const items = this.items;
|
|
392
|
-
const current =
|
|
415
|
+
const current = this.named(items);
|
|
393
416
|
return current
|
|
394
417
|
? formatWords(this, 'statusLabel', {
|
|
395
418
|
count: items.length,
|