@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.
Files changed (44) hide show
  1. package/custom-elements.json +198 -59
  2. package/dist/app-header/root/app-header-root.d.ts +2 -1
  3. package/dist/app-header/root/app-header-root.js +6 -1
  4. package/dist/button/root/button-root.d.ts +6 -1
  5. package/dist/button/root/button-root.js +1 -0
  6. package/dist/command/root/command-root.d.ts +4 -2
  7. package/dist/command/root/command-root.js +1 -0
  8. package/dist/date-picker/root/date-picker-root.d.ts +6 -0
  9. package/dist/date-picker/root/date-picker-root.js +7 -1
  10. package/dist/dialog/form-method.d.ts +12 -0
  11. package/dist/dialog/form-method.js +13 -0
  12. package/dist/dialog/root/dialog-root.d.ts +1 -9
  13. package/dist/dialog/root/dialog-root.js +2 -11
  14. package/dist/drawer/root/drawer-root.d.ts +2 -0
  15. package/dist/drawer/root/drawer-root.js +6 -0
  16. package/dist/filter-input/index.d.ts +0 -1
  17. package/dist/filter-input/index.js +0 -1
  18. package/dist/filter-input/menu/filter-menu.d.ts +4 -0
  19. package/dist/filter-input/menu/index.d.ts +1 -0
  20. package/dist/filter-input/menu/index.js +1 -0
  21. package/dist/filter-input/root/filter-input-root.d.ts +26 -2
  22. package/dist/filter-input/root/filter-input-root.js +39 -5
  23. package/dist/icon/all/index.d.ts +1 -0
  24. package/dist/icon/all/index.js +4 -0
  25. package/dist/icon/root/icon-root.d.ts +9 -8
  26. package/dist/icon/root/icon-root.js +4 -2
  27. package/dist/icon/set.js +16 -2
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.js +2 -1
  30. package/dist/lightbox/item/lightbox-item.d.ts +1 -0
  31. package/dist/lightbox/root/lightbox-root.d.ts +25 -5
  32. package/dist/lightbox/root/lightbox-root.js +33 -10
  33. package/dist/notifications/group/notification-group.js +2 -2
  34. package/dist/notifications/notification/notification.js +4 -5
  35. package/dist/notifications/root/notifications-root.d.ts +4 -3
  36. package/dist/notifications/root/notifications-root.js +1 -1
  37. package/dist/pagination/root/pagination-root.d.ts +2 -1
  38. package/dist/pagination/root/pagination-root.js +6 -1
  39. package/dist/tooltip/root/tooltip-root.d.ts +5 -2
  40. package/dist/tooltip/root/tooltip-root.js +11 -11
  41. package/lint.json +1 -1
  42. package/package.json +5 -5
  43. package/types/jsx.d.ts +102 -72
  44. package/types/vue.d.ts +13 -14
@@ -198,6 +198,7 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
198
198
  return;
199
199
  }
200
200
  if (this.open) {
201
+ this.listbox?.highlight('none');
201
202
  this.searchField?.focus();
202
203
  }
203
204
  else {
@@ -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
- asDate(todayIn(this.timeZone)),
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
- const form = event.target;
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) {
@@ -1,6 +1,5 @@
1
1
  export * from './chip/filter-chip.js';
2
2
  export * from './field/filter-field.js';
3
3
  export * from './grammar.js';
4
- export * from './menu/filter-menu.js';
5
4
  export * from './operator/filter-operator.js';
6
5
  export * from './root/filter-input-root.js';
@@ -1,6 +1,5 @@
1
1
  export * from './chip/filter-chip.js';
2
2
  export * from './field/filter-field.js';
3
3
  export * from './grammar.js';
4
- export * from './menu/filter-menu.js';
5
4
  export * from './operator/filter-operator.js';
6
5
  export * from './root/filter-input-root.js';
@@ -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. `disposition:refunded,settled` and `-disposition:refunded` fire
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 instanceof HelixHeadlessFilterMenu)) {
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';
@@ -0,0 +1,4 @@
1
+ import { icons } from '@fusion.dev/helix-icons';
2
+ import { registerIcons } from '../registry.js';
3
+ registerIcons(icons);
4
+ 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 the
22
- * whole set, so any name in it draws. The element's own entry,
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 of the
98
- * whole set that no bundler can shake: 15 kB for a consumer who renders one
99
- * mark. An attribute is the only thing an HTML author can write, and that is
100
- * worth the index; a consumer who would rather not pay it imports the
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
- 'nothing. Check the name, register the glyph with `registerIcons`, or import it ' +
135
- 'and pass it as `icon`.'
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 { icons } from '@fusion.dev/helix-icons';
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(icons);
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, the
37
- * live region says the new scale in the words of `zoom-status-label`, such as
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
- /** Says the new scale in the live region, a moment after the last change. */
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.value);
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.value);
288
+ this.requestActive(keyOf(target, at));
278
289
  }
279
290
  }
280
291
  get shown() {
281
- return resolveActiveItem(this.active ?? '', this.items);
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 = resolveActiveItem(this.active, items);
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 = resolveActiveItem(this.active, items);
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 = resolveActiveItem(this.active, items);
415
+ const current = this.named(items);
393
416
  return current
394
417
  ? formatWords(this, 'statusLabel', {
395
418
  count: items.length,