@fusion.dev/helix-headless 0.157.0 → 0.159.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 (56) hide show
  1. package/custom-elements.json +851 -56
  2. package/dist/app-header/root/app-header-root.d.ts +59 -12
  3. package/dist/app-header/root/app-header-root.js +68 -29
  4. package/dist/button/root/button-root.d.ts +8 -0
  5. package/dist/button/root/button-root.js +4 -0
  6. package/dist/button-group/root/button-group-root.d.ts +20 -7
  7. package/dist/button-group/root/button-group-root.js +30 -5
  8. package/dist/command/root/command-root.d.ts +19 -5
  9. package/dist/command/root/command-root.js +14 -4
  10. package/dist/data-table/root/data-table-root.js +5 -2
  11. package/dist/element-events.generated.js +7 -1
  12. package/dist/lightbox/item/lightbox-item.d.ts +7 -1
  13. package/dist/lightbox/item/lightbox-item.js +15 -2
  14. package/dist/lightbox/root/lightbox-root.d.ts +8 -4
  15. package/dist/lightbox/root/lightbox-root.js +9 -1
  16. package/dist/lightbox/zoom-controller.d.ts +13 -4
  17. package/dist/lightbox/zoom-controller.js +17 -0
  18. package/dist/list/item/list-item.d.ts +15 -1
  19. package/dist/list/item/list-item.js +20 -1
  20. package/dist/list/root/list-root.d.ts +29 -1
  21. package/dist/list/root/list-root.js +44 -10
  22. package/dist/listbox/root/listbox-root.d.ts +19 -2
  23. package/dist/listbox/root/listbox-root.js +16 -8
  24. package/dist/map/root/map-root.d.ts +4 -0
  25. package/dist/map/root/map-root.js +1 -1
  26. package/dist/menu/item/menu-item.js +1 -1
  27. package/dist/menu/root/menu-root.d.ts +6 -2
  28. package/dist/menu/root/menu-root.js +9 -2
  29. package/dist/number-input/root/number-input-root.d.ts +7 -4
  30. package/dist/number-input/root/number-input-root.js +3 -3
  31. package/dist/progress/root/progress-root.js +1 -1
  32. package/dist/select/root/select-root.d.ts +8 -1
  33. package/dist/select/root/select-root.js +8 -5
  34. package/dist/skeleton/root/skeleton-root.d.ts +2 -1
  35. package/dist/skeleton/root/skeleton-root.js +1 -1
  36. package/dist/steps/root/steps-root.js +8 -6
  37. package/dist/steps/step/step.d.ts +9 -0
  38. package/dist/steps/step/step.js +19 -2
  39. package/dist/tabs/panel/tab-panel.d.ts +22 -1
  40. package/dist/tabs/panel/tab-panel.js +22 -1
  41. package/dist/tabs/root/tabs-root.d.ts +21 -5
  42. package/dist/tabs/root/tabs-root.js +24 -13
  43. package/dist/tabs/tab/tab.d.ts +4 -2
  44. package/dist/tabs/tab/tab.js +3 -1
  45. package/dist/toast/item/toast-item.d.ts +27 -8
  46. package/dist/toast/item/toast-item.js +47 -11
  47. package/dist/toast/root/toast-root.d.ts +24 -1
  48. package/dist/toast/root/toast-root.js +54 -0
  49. package/dist/tour/root/tour-root.d.ts +34 -0
  50. package/dist/tour/root/tour-root.js +92 -3
  51. package/dist/tour/stop/tour-stop.d.ts +33 -1
  52. package/dist/tour/stop/tour-stop.js +12 -1
  53. package/lint.json +1 -1
  54. package/package.json +5 -5
  55. package/types/jsx.d.ts +667 -102
  56. package/types/vue.d.ts +148 -23
@@ -33,10 +33,14 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
33
33
  this.manual = false;
34
34
  this.scrollControls = false;
35
35
  this.handleSelection = (event) => {
36
- if (event.defaultPrevented || !this.tabs.includes(event.target)) {
36
+ if (!this.tabs.includes(event.target)) {
37
37
  return;
38
38
  }
39
- this.requestActive(event.detail.value);
39
+ queueMicrotask(() => {
40
+ if (!event.defaultPrevented) {
41
+ this.requestActive(event.detail.value);
42
+ }
43
+ });
40
44
  };
41
45
  this.more = { end: false, start: false };
42
46
  this.measure = () => {
@@ -89,12 +93,19 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
89
93
  const behavior = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
90
94
  strip.scrollBy(down ? { behavior, top: by } : { behavior, left: isRtl(this) ? -by : by });
91
95
  }
96
+ beside(gone, tabs) {
97
+ const at = this.held.indexOf(gone);
98
+ const by = new Map(tabs.map((tab) => [tab.value, tab]));
99
+ const order = [...this.held.slice(at + 1), ...this.held.slice(0, at).reverse()];
100
+ return order.map((value) => by.get(value)).find((tab) => tab !== undefined && !tab.disabled);
101
+ }
92
102
  sync() {
93
103
  const tabs = this.tabs;
94
104
  const declared = this.active === '' ? tabs.find((tab) => tab.selected) : undefined;
95
- const value = (declared ?? this.resolveActive(tabs))?.value ?? '';
96
105
  const previous = this.active;
97
106
  const gone = this.held.includes(previous) && !tabs.some((tab) => tab.value === previous);
107
+ const value = (declared ?? (gone ? this.beside(previous, tabs) : undefined) ?? this.resolveActive(tabs))
108
+ ?.value ?? '';
98
109
  this.held = tabs.map((tab) => tab.value);
99
110
  if (gone && value !== '') {
100
111
  this.active = value;
@@ -192,19 +203,19 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
192
203
  if (!this.scrollControls) {
193
204
  return nothing;
194
205
  }
195
- const down = this.orientation === 'vertical';
196
- const icon = down ? (toward > 0 ? caretDown : caretUp) : toward > 0 ? caretRight : caretLeft;
197
- const glyph = html `<svg
198
- data-mirror=${down ? nothing : ''}
199
- aria-hidden="true"
200
- fill="currentColor"
201
- viewBox=${icon.viewBox}
202
- >${unsafeSVG(icon.content)}</svg>`;
203
206
  const shown = toward > 0 ? this.more.end : this.more.start;
204
207
  const keepFocus = (event) => event.preventDefault();
205
208
  return toward > 0
206
- ? html `<button part="scroll-end" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(1)}>${glyph}</button>`
207
- : html `<button part="scroll-start" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(-1)}>${glyph}</button>`;
209
+ ? html `<button part="scroll-end" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(1)}>${this.renderScrollIcon(1)}</button>`
210
+ : html `<button part="scroll-start" type="button" tabindex="-1" aria-hidden="true" ?hidden=${!shown} @mousedown=${keepFocus} @click=${() => this.scrollStrip(-1)}>${this.renderScrollIcon(-1)}</button>`;
211
+ }
212
+ renderScrollIcon(toward) {
213
+ const down = this.orientation === 'vertical';
214
+ const icon = down ? (toward > 0 ? caretDown : caretUp) : toward > 0 ? caretRight : caretLeft;
215
+ const mirror = down ? nothing : '';
216
+ return toward > 0
217
+ ? html `<slot name="scroll-end-icon"><svg part="scroll-end-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}>${unsafeSVG(icon.content)}</svg></slot>`
218
+ : html `<slot name="scroll-start-icon"><svg part="scroll-start-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}>${unsafeSVG(icon.content)}</svg></slot>`;
208
219
  }
209
220
  render() {
210
221
  return html `<div
@@ -16,8 +16,8 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
16
16
  * can: a press on its `dismiss` mark, or Delete or Backspace while the tab
17
17
  * has focus, sends `helix-dismiss`. The tab does not remove itself, because
18
18
  * your code owns the set, so remove the tab and its panel in your handler.
19
- * Focus moves to the tab beside it, and the tab set selects a tab that is
20
- * left.
19
+ * Focus moves to the tab beside it, and where the tab was selected, the tab
20
+ * set selects that same tab.
21
21
  * The tab says the key through `aria-keyshortcuts`, and the mark is for the
22
22
  * pointer, hidden from screen readers.
23
23
  *
@@ -27,11 +27,13 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
27
27
  * @documentation https://fusion.dev/helix/components/tabs
28
28
  *
29
29
  * @slot - The tab's label.
30
+ * @slot dismiss-icon - Replaces the glyph on the mark that closes a `dismissible` tab.
30
31
  *
31
32
  * @event {HelixDismissEvent} helix-dismiss - A `dismissible` tab was asked to close. The tab does not remove itself, so remove it and its panel in response. Cancelable.
32
33
  * @event {HelixSelectEvent} helix-select - Fired when the tab is chosen. Cancelable: canceling it stops the change and leaves the decision to you.
33
34
  *
34
35
  * @csspart dismiss - The mark a pointer presses to close a `dismissible` tab.
36
+ * @csspart dismiss-mark - The glyph on that mark, where none is slotted.
35
37
  * @csspart tab - The tab itself.
36
38
  *
37
39
  * @cssstate selected - While it is the tab selected.
@@ -53,7 +53,9 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinDismissible(MixinSele
53
53
  return nothing;
54
54
  }
55
55
  return html `<span part="dismiss" aria-hidden="true" title=${this.dismissLabel} @click=${this.handleDismissPress}
56
- ><svg fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">${unsafeSVG(x.content)}</svg></span
56
+ ><slot name="dismiss-icon"
57
+ ><svg part="dismiss-mark" fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">${unsafeSVG(x.content)}</svg></slot
58
+ ></span
57
59
  >`;
58
60
  }
59
61
  updated(changed) {
@@ -10,8 +10,9 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
10
10
  *
11
11
  * **Each item runs its own timer.** A timer held by the region would restart
12
12
  * with each new message, and the last of three messages would stay for nine
13
- * seconds. It pauses while the pointer or focus is on the message, and while
14
- * the page is hidden, so a message is not spent in a background tab.
13
+ * seconds. It pauses while the pointer or focus is on the message or anywhere
14
+ * in the region's stack, and while the page is hidden, so a message is not
15
+ * spent in a background tab.
15
16
  *
16
17
  * **Each message is an `alertdialog` named by its words**, which is how a reader
17
18
  * who moves into the stack hears which message they are on. Give it a
@@ -20,10 +21,11 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
20
21
  * says each message once as it arrives, a `danger` one assertively, so a
21
22
  * message is never a live region of its own.
22
23
  *
23
- * A touch or a pen swipes a message toward the end of the line, and past
24
- * halfway, or quickly, dismisses it through the same `helix-dismiss` as its
25
- * control. While it moves, the distance is on the `item` part as
26
- * `--toast-swipe-x`.
24
+ * A touch or a pen swipes a message toward the end of the line, or toward the
25
+ * edge the region's `swipe-direction` names, and past halfway, or quickly,
26
+ * dismisses it through the same `helix-dismiss` as its control. While it
27
+ * moves, the distance is on the `item` part as `--toast-swipe-x` or
28
+ * `--toast-swipe-y`.
27
29
  *
28
30
  * **Escape dismisses the message holding focus**, through the same
29
31
  * `helix-dismiss`, and focus moves to the next message.
@@ -60,9 +62,10 @@ declare const HelixHeadlessToastItem_base: (new (...args: any[]) => import("@fus
60
62
  *
61
63
  * @cssproperty --toast-duration - How long this message stays, in milliseconds, for a progress hint.
62
64
  * @cssproperty --toast-swipe-x - How far a swipe has moved the message across, in pixels, toward the right.
65
+ * @cssproperty --toast-swipe-y - How far a swipe has moved the message down, in pixels, under a `swipe-direction` of `top` or `bottom`.
63
66
  *
64
67
  * @cssstate closed - The message has been dismissed and is hidden, or leaving.
65
- * @cssstate paused - The dismiss timer is paused, while a pointer or focus is on the message, or while the page is hidden.
68
+ * @cssstate paused - The dismiss timer is paused, while a pointer or focus is on the message or in the stack, or while the page is hidden.
66
69
  * @cssstate waiting - The region holds the message back, past its `max-visible`, until a message before it goes.
67
70
  */
68
71
  export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base {
@@ -96,8 +99,24 @@ export declare class HelixHeadlessToastItem extends HelixHeadlessToastItem_base
96
99
  */
97
100
  get open(): boolean;
98
101
  set open(next: boolean);
102
+ /**
103
+ * @internal Set by the region: whether a pointer or focus in the stack holds
104
+ * this message's timer.
105
+ */
106
+ regionHeld: boolean;
107
+ /**
108
+ * @internal Set by the region: the edge a swipe dismisses toward, from its
109
+ * `swipe-direction`.
110
+ */
111
+ swipeToward: 'bottom' | 'end' | 'start' | 'top';
112
+ /** Whether the timer is held for `regionHeld`, so the hold is let go once. */
113
+ private regionHolding;
114
+ /** Holds the timer while the region asks, and lets it go once. */
115
+ private holdForRegion;
116
+ /** The physical edge a swipe dismisses toward. */
117
+ private get swipeTo();
99
118
  protected readonly swipes: SwipeController;
100
- /** Writes how far a swipe has moved the message toward the end of the line. */
119
+ /** Writes how far a swipe has moved the message toward its edge. */
101
120
  private slide;
102
121
  /**
103
122
  * The dismiss timer, which pauses while a reader is on the message, and
@@ -4,30 +4,34 @@ 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 { CountdownController, customElement, drawDismiss, drawDismissGlyph, HelixElement, hostAria, isRtl, MixinDismissible, requestChange, SwipeController, settleSwipe, swipeEdge, toneMark, towardEdge, } from '@fusion.dev/helix-core';
7
+ import { CountdownController, customElement, drawDismiss, drawDismissGlyph, edgeOffset, HelixElement, hostAria, isRtl, MixinDismissible, requestChange, SwipeController, settleSwipe, swipeAxis, swipeEdge, toneMark, towardEdge, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
11
11
  let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissible(HelixElement) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.regionHeld = false;
15
+ this.swipeToward = 'end';
16
+ this.regionHolding = false;
14
17
  this.swipes = new SwipeController(this, {
15
- axis: () => (this.open ? 'x' : undefined),
16
- end: (dx, _dy, vx) => {
17
- const edge = swipeEdge('end', isRtl(this));
18
- const width = this.partNamed('item')?.getBoundingClientRect().width ?? 0;
18
+ axis: () => (this.open ? swipeAxis(this.swipeTo) : undefined),
19
+ end: (dx, dy, vx, vy) => {
20
+ const edge = this.swipeTo;
21
+ const box = this.partNamed('item')?.getBoundingClientRect();
22
+ const across = swipeAxis(edge) === 'x';
19
23
  const shown = settleSwipe({
20
24
  points: [1],
21
- size: width,
22
- travel: Math.max(0, towardEdge(edge, dx, 0)),
23
- velocity: towardEdge(edge, vx, 0),
25
+ size: (across ? box?.width : box?.height) ?? 0,
26
+ travel: Math.max(0, towardEdge(edge, dx, dy)),
27
+ velocity: towardEdge(edge, vx, vy),
24
28
  });
25
29
  if (shown === 0 && this.dismiss()) {
26
30
  return;
27
31
  }
28
32
  this.slide(0);
29
33
  },
30
- move: (dx) => this.slide(Math.max(0, towardEdge(swipeEdge('end', isRtl(this)), dx, 0))),
34
+ move: (dx, dy) => this.slide(Math.max(0, towardEdge(this.swipeTo, dx, dy))),
31
35
  start: (_target, event) => event.pointerType !== 'mouse',
32
36
  surface: () => this.partNamed('item'),
33
37
  });
@@ -81,9 +85,27 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
81
85
  this.openState = next;
82
86
  this.requestUpdate('open', previous);
83
87
  }
88
+ holdForRegion() {
89
+ const want = this.regionHeld && this.isConnected;
90
+ if (want === this.regionHolding) {
91
+ return;
92
+ }
93
+ this.regionHolding = want;
94
+ if (want) {
95
+ this.countdown.hold();
96
+ }
97
+ else {
98
+ this.countdown.release();
99
+ }
100
+ }
101
+ get swipeTo() {
102
+ return swipeEdge(this.swipeToward, isRtl(this));
103
+ }
84
104
  slide(travel) {
85
- const physical = isRtl(this) ? -travel : travel;
86
- this.partNamed('item')?.style.setProperty('--toast-swipe-x', travel === 0 ? '' : `${Math.round(physical)}px`);
105
+ const item = this.partNamed('item');
106
+ const { x, y } = edgeOffset(this.swipeTo, travel);
107
+ item?.style.setProperty('--toast-swipe-x', x === 0 ? '' : `${Math.round(x)}px`);
108
+ item?.style.setProperty('--toast-swipe-y', y === 0 ? '' : `${Math.round(y)}px`);
87
109
  }
88
110
  wait(waiting) {
89
111
  if (waiting) {
@@ -133,12 +155,14 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
133
155
  else {
134
156
  this.resume();
135
157
  }
158
+ this.holdForRegion();
136
159
  }
137
160
  this.addEventListener('keydown', this.handleKeydown);
138
161
  }
139
162
  disconnectedCallback() {
140
163
  super.disconnectedCallback();
141
164
  this.holding = false;
165
+ this.regionHolding = false;
142
166
  this.removeEventListener('keydown', this.handleKeydown);
143
167
  }
144
168
  willUpdate(changed) {
@@ -156,6 +180,12 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
156
180
  if (changed.has('waiting')) {
157
181
  this.wait(this.waiting);
158
182
  }
183
+ if (changed.has('regionHeld')) {
184
+ this.holdForRegion();
185
+ }
186
+ if (changed.has('swipeToward')) {
187
+ this.partNamed('item')?.style.setProperty('touch-action', swipeAxis(this.swipeTo) === 'y' ? 'pan-x' : '');
188
+ }
159
189
  }
160
190
  assigned(name) {
161
191
  return (this.renderRoot
@@ -229,6 +259,12 @@ __decorate([
229
259
  __decorate([
230
260
  property({ type: Boolean })
231
261
  ], HelixHeadlessToastItem.prototype, "open", null);
262
+ __decorate([
263
+ property({ attribute: false, type: Boolean })
264
+ ], HelixHeadlessToastItem.prototype, "regionHeld", void 0);
265
+ __decorate([
266
+ property({ attribute: false })
267
+ ], HelixHeadlessToastItem.prototype, "swipeToward", void 0);
232
268
  __decorate([
233
269
  property({ reflect: true })
234
270
  ], HelixHeadlessToastItem.prototype, "tone", void 0);
@@ -34,7 +34,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
34
34
  * to render them from: it appends a message of the region's own tier and
35
35
  * removes it once it has been dismissed and has finished leaving. Messages
36
36
  * you render yourself stay yours, and the region never removes them. A touch
37
- * or a pen swipes a message away toward the end of the line.
37
+ * or a pen swipes a message away toward the end of the line, or toward the
38
+ * edge `swipe-direction` names.
39
+ *
40
+ * **Every message's timer waits while a pointer or focus is in the stack**, as
41
+ * React Aria's and Radix's regions do, so a reader moving from one message to
42
+ * the next does not lose the one behind them.
38
43
  *
39
44
  * **The stack is in the top layer.** The `region` part is a `popover="manual"`
40
45
  * shown while a message is showing, and shown again above a modal dialog that
@@ -127,6 +132,11 @@ export declare class HelixHeadlessToast extends HelixElement {
127
132
  * departures, which is what `orientation` does.
128
133
  */
129
134
  placement?: 'bottom-center' | 'bottom-end' | 'bottom-start' | 'top-center' | 'top-end' | 'top-start';
135
+ /**
136
+ * The edge a touch or a pen swipes a message toward to dismiss it: `end`,
137
+ * the end of the line, or `start`, `top` or `bottom`. Unset is `end`.
138
+ */
139
+ swipeDirection?: 'bottom' | 'end' | 'start' | 'top';
130
140
  protected willUpdate(changed: PropertyValues): void;
131
141
  /** What the polite region is saying. */
132
142
  private said;
@@ -190,6 +200,19 @@ export declare class HelixHeadlessToast extends HelixElement {
190
200
  private watcher;
191
201
  /** The messages that are showing, in order. */
192
202
  private get showing();
203
+ /** Whether a pointer is over the stack. */
204
+ private pointerIn;
205
+ /** Whether focus is in the stack. */
206
+ private focusIn;
207
+ /**
208
+ * Tells every message whether the stack holds its timer and which way it
209
+ * swipes, which only the region knows.
210
+ */
211
+ private inform;
212
+ private readonly handlePointerEnter;
213
+ private readonly handlePointerLeave;
214
+ /** Focus leaving the stack, and not moving between its messages. */
215
+ private readonly handleFocusOut;
193
216
  /** The messages `max-visible` holds back. */
194
217
  private readonly held;
195
218
  /**
@@ -40,12 +40,34 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
40
40
  }
41
41
  };
42
42
  this.made = new WeakSet();
43
+ this.pointerIn = false;
44
+ this.focusIn = false;
45
+ this.handlePointerEnter = () => {
46
+ this.pointerIn = true;
47
+ this.inform();
48
+ };
49
+ this.handlePointerLeave = () => {
50
+ this.pointerIn = false;
51
+ this.inform();
52
+ };
53
+ this.handleFocusOut = (event) => {
54
+ const next = event.relatedTarget;
55
+ if (next instanceof Node && deepContains(this, next)) {
56
+ return;
57
+ }
58
+ this.focusIn = false;
59
+ this.inform();
60
+ };
43
61
  this.held = new WeakSet();
44
62
  this.handleFocusIn = (event) => {
45
63
  const from = event.relatedTarget;
46
64
  if (from instanceof HTMLElement && !deepContains(this, from)) {
47
65
  this.focusOrigin = from;
48
66
  }
67
+ if (!this.focusIn) {
68
+ this.focusIn = true;
69
+ this.inform();
70
+ }
49
71
  };
50
72
  this.handleDismiss = (event) => {
51
73
  const item = event.target;
@@ -177,6 +199,22 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
177
199
  get showing() {
178
200
  return [...this.children].filter((one) => one instanceof HTMLElement && !one.hidden && !this.held.has(one));
179
201
  }
202
+ inform() {
203
+ const held = this.pointerIn || this.focusIn;
204
+ for (const one of this.children) {
205
+ const item = one;
206
+ if (!('regionHeld' in item)) {
207
+ continue;
208
+ }
209
+ if (item.regionHeld !== held) {
210
+ item.regionHeld = held;
211
+ }
212
+ const toward = this.swipeDirection ?? 'end';
213
+ if (item.swipeToward !== toward) {
214
+ item.swipeToward = toward;
215
+ }
216
+ }
217
+ }
180
218
  queue() {
181
219
  const max = this.maxVisible;
182
220
  const line = [...this.children].filter((one) => one instanceof HTMLElement && one.open !== false);
@@ -267,11 +305,15 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
267
305
  connectedCallback() {
268
306
  super.connectedCallback();
269
307
  this.addEventListener('focusin', this.handleFocusIn);
308
+ this.addEventListener('focusout', this.handleFocusOut);
309
+ this.addEventListener('pointerenter', this.handlePointerEnter);
310
+ this.addEventListener('pointerleave', this.handlePointerLeave);
270
311
  this.addEventListener('helix-dismiss', this.handleDismiss);
271
312
  document.addEventListener('keydown', this.handleHotkey);
272
313
  this.watcher = new MutationObserver((records) => {
273
314
  const added = records.flatMap((record) => [...record.addedNodes]);
274
315
  this.restack();
316
+ this.inform();
275
317
  this.place(added);
276
318
  this.tell(added);
277
319
  this.sweep();
@@ -289,7 +331,12 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
289
331
  disconnectedCallback() {
290
332
  super.disconnectedCallback();
291
333
  this.removeEventListener('focusin', this.handleFocusIn);
334
+ this.removeEventListener('focusout', this.handleFocusOut);
335
+ this.removeEventListener('pointerenter', this.handlePointerEnter);
336
+ this.removeEventListener('pointerleave', this.handlePointerLeave);
292
337
  this.removeEventListener('helix-dismiss', this.handleDismiss);
338
+ this.pointerIn = false;
339
+ this.focusIn = false;
293
340
  document.removeEventListener('keydown', this.handleHotkey);
294
341
  this.watcher?.disconnect();
295
342
  this.watcher = undefined;
@@ -297,6 +344,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
297
344
  firstUpdated(changed) {
298
345
  super.firstUpdated(changed);
299
346
  this.queue();
347
+ this.inform();
300
348
  this.place();
301
349
  }
302
350
  updated(changed) {
@@ -305,6 +353,9 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
305
353
  this.restack();
306
354
  this.place();
307
355
  }
356
+ if (changed.has('swipeDirection')) {
357
+ this.inform();
358
+ }
308
359
  }
309
360
  render() {
310
361
  return html `<div part="region" popover="manual" tabindex="-1"><slot></slot><span part="live" role="status">${this.said}</span><span part="live" role="alert">${this.urgent}</span></div>`;
@@ -323,6 +374,9 @@ __decorate([
323
374
  __decorate([
324
375
  property({ reflect: true })
325
376
  ], HelixHeadlessToast.prototype, "placement", void 0);
377
+ __decorate([
378
+ property({ attribute: 'swipe-direction' })
379
+ ], HelixHeadlessToast.prototype, "swipeDirection", void 0);
326
380
  __decorate([
327
381
  state()
328
382
  ], HelixHeadlessToast.prototype, "said", void 0);
@@ -44,6 +44,17 @@ declare const HelixHeadlessTour_base: (new (...args: any[]) => import("@fusion.d
44
44
  * least distance that brings it on screen, so the stop that points at it is
45
45
  * on screen too.
46
46
  *
47
+ * **A stop's `effect` runs as the tour reaches it**, before it is shown, and
48
+ * the stop waits for a promise it returns, with `:state(preparing)` on the
49
+ * tour meanwhile. A function it returns runs when the tour leaves the stop.
50
+ *
51
+ * **A press outside the current stop and its target does nothing by
52
+ * default**, so a reader can use the page the tour is describing. Set
53
+ * `outside-press="close"` to end the tour on one, through `helix-before-close`
54
+ * as Skip does, or `outside-press="block"` to refuse it, so the stop and its
55
+ * target are all a pointer can reach while the tour runs, as Zag's
56
+ * `preventInteraction` does.
57
+ *
47
58
  * **Keyboard.** Focus is on the current stop's panel, which is not in the
48
59
  * `Tab` order itself.
49
60
  *
@@ -89,6 +100,7 @@ declare const HelixHeadlessTour_base: (new (...args: any[]) => import("@fusion.d
89
100
  * @cssproperty --tour-spotlight-y - The lit box's top edge, from the window's top edge, in pixels. Written on the spotlight while it lights a target.
90
101
  *
91
102
  * @cssstate open - While the tour is running.
103
+ * @cssstate preparing - While the tour waits for the current stop's `effect` before showing the stop.
92
104
  * @cssstate waiting - While the tour waits at a `wait` stop for its target to be on the page.
93
105
  */
94
106
  export declare class HelixHeadlessTour extends HelixHeadlessTour_base {
@@ -134,6 +146,12 @@ export declare class HelixHeadlessTour extends HelixHeadlessTour_base {
134
146
  * Label for the control that goes on.
135
147
  */
136
148
  nextLabel: string;
149
+ /**
150
+ * What a press outside the current stop and its target does while the tour
151
+ * runs. Unset, nothing: the page stays usable. `close` ends the tour, as
152
+ * Skip does, and `block` refuses the press.
153
+ */
154
+ outsidePress?: 'block' | 'close';
137
155
  /**
138
156
  * Draws each stop's place in the run in its panel, as well as saying it.
139
157
  */
@@ -201,6 +219,22 @@ export declare class HelixHeadlessTour extends HelixHeadlessTour_base {
201
219
  connectedCallback(): void;
202
220
  disconnectedCallback(): void;
203
221
  protected willUpdate(changed: PropertyValues): void;
222
+ /**
223
+ * Answers a press outside the current stop and its target, as
224
+ * `outside-press` asks. A press on the tour's own parts, the stop or the
225
+ * target is the reader using the tour.
226
+ */
227
+ private readonly handleOutside;
228
+ /** The stop whose `effect` has run, and the function to run on leaving it. */
229
+ private entered;
230
+ /** The stop held back while its `effect` settles. */
231
+ private preparing;
232
+ /**
233
+ * Runs the current stop's `effect` as the tour reaches it, and the last
234
+ * one's leaving function as it leaves, and holds the stop back while a
235
+ * promise settles.
236
+ */
237
+ private enter;
204
238
  protected updated(changed: PropertyValues): void;
205
239
  /** The target last scrolled into view, so a render that moved nothing scrolls nothing. */
206
240
  private revealed;
@@ -59,6 +59,25 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
59
59
  this.handleViewport = () => {
60
60
  this.light();
61
61
  };
62
+ this.handleOutside = (event) => {
63
+ const mode = this.outsidePress;
64
+ if (!this.open || !mode) {
65
+ return;
66
+ }
67
+ const path = event.composedPath();
68
+ const stop = this.current;
69
+ const target = stop?.type === undefined ? stop?.target : undefined;
70
+ if (path.includes(this) || (target && path.includes(target))) {
71
+ return;
72
+ }
73
+ if (mode === 'block') {
74
+ event.preventDefault();
75
+ event.stopPropagation();
76
+ }
77
+ else if (event.type === 'pointerdown') {
78
+ this.skip();
79
+ }
80
+ };
62
81
  this.followed = false;
63
82
  this.veiled = false;
64
83
  }
@@ -122,6 +141,8 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
122
141
  this.addEventListener('helix-tour-ask', this.handleAsk);
123
142
  this.addEventListener('keydown', this.handleArrows);
124
143
  document.addEventListener('keydown', this.handleKeydown);
144
+ document.addEventListener('pointerdown', this.handleOutside, true);
145
+ document.addEventListener('click', this.handleOutside, true);
125
146
  window.addEventListener('resize', this.handleViewport, { passive: true });
126
147
  window.addEventListener('scroll', this.handleViewport, { capture: true, passive: true });
127
148
  if (this.hasUpdated) {
@@ -133,6 +154,9 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
133
154
  this.removeEventListener('helix-tour-ask', this.handleAsk);
134
155
  this.removeEventListener('keydown', this.handleArrows);
135
156
  document.removeEventListener('keydown', this.handleKeydown);
157
+ document.removeEventListener('pointerdown', this.handleOutside, true);
158
+ document.removeEventListener('click', this.handleOutside, true);
159
+ this.enter(undefined);
136
160
  window.removeEventListener('resize', this.handleViewport);
137
161
  window.removeEventListener('scroll', this.handleViewport, { capture: true });
138
162
  this.sizer?.disconnect();
@@ -149,8 +173,67 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
149
173
  role: this.open ? 'region' : '',
150
174
  });
151
175
  }
176
+ enter(stop) {
177
+ if (stop === this.entered?.stop) {
178
+ return;
179
+ }
180
+ const left = this.entered;
181
+ this.entered = stop ? { stop } : undefined;
182
+ left?.leave?.();
183
+ this.preparing = undefined;
184
+ this.toggleState('preparing', false);
185
+ const effect = stop?.effect;
186
+ if (!stop || !effect) {
187
+ return;
188
+ }
189
+ const ran = effect({
190
+ index: this.index,
191
+ next: () => this.next(),
192
+ skip: () => this.skip(),
193
+ target: stop.target,
194
+ });
195
+ const keep = (leave) => {
196
+ if (typeof leave !== 'function') {
197
+ return;
198
+ }
199
+ if (this.entered?.stop === stop) {
200
+ this.entered.leave = leave;
201
+ }
202
+ else {
203
+ ;
204
+ leave();
205
+ }
206
+ };
207
+ if (typeof ran?.then !== 'function') {
208
+ keep(ran);
209
+ return;
210
+ }
211
+ this.preparing = stop;
212
+ this.toggleState('preparing', true);
213
+ void ran
214
+ .then(keep, () => undefined)
215
+ .then(() => {
216
+ if (this.preparing !== stop) {
217
+ return;
218
+ }
219
+ this.preparing = undefined;
220
+ this.toggleState('preparing', false);
221
+ this.requestUpdate();
222
+ void this.updateComplete.then(() => {
223
+ const active = deepActiveElement();
224
+ const waiting = active === null ||
225
+ active === document.body ||
226
+ active === this.returnFocus ||
227
+ deepContains(this, active);
228
+ if (waiting) {
229
+ this.focusCurrent();
230
+ }
231
+ });
232
+ });
233
+ }
152
234
  updated(changed) {
153
235
  super.updated(changed);
236
+ this.enter(this.open ? this.current : undefined);
154
237
  this.reveal();
155
238
  this.settle();
156
239
  this.light();
@@ -228,7 +311,7 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
228
311
  const at = Math.max(0, Math.min(this.index, stops.length - 1));
229
312
  stops.forEach((stop, index) => {
230
313
  whenHydrated(stop, () => {
231
- stop.current = this.open && index === at;
314
+ stop.current = this.open && index === at && this.preparing !== stop;
232
315
  stop.first = index === 0;
233
316
  stop.last = index === stops.length - 1;
234
317
  stop.position = formatWords(this, 'progressLabel', {
@@ -253,7 +336,9 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
253
336
  }
254
337
  get lit() {
255
338
  const stop = this.current;
256
- return this.open && stop?.type === undefined ? stop?.target : undefined;
339
+ return this.open && stop?.type === undefined && this.preparing !== stop
340
+ ? stop?.target
341
+ : undefined;
257
342
  }
258
343
  get current() {
259
344
  const stops = this.stops;
@@ -285,7 +370,8 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
285
370
  });
286
371
  }
287
372
  light() {
288
- const stop = this.open ? this.current : undefined;
373
+ const shown = this.current;
374
+ const stop = this.open && this.preparing !== shown ? shown : undefined;
289
375
  const veiled = stop !== undefined && stop.type !== 'wait' && (stop.type !== undefined || !stop.target);
290
376
  if (veiled !== this.veiled) {
291
377
  this.veiled = veiled;
@@ -355,6 +441,9 @@ __decorate([
355
441
  __decorate([
356
442
  property({ attribute: 'next-label' })
357
443
  ], HelixHeadlessTour.prototype, "nextLabel", void 0);
444
+ __decorate([
445
+ property({ attribute: 'outside-press' })
446
+ ], HelixHeadlessTour.prototype, "outsidePress", void 0);
358
447
  __decorate([
359
448
  property({ type: Boolean })
360
449
  ], HelixHeadlessTour.prototype, "progress", void 0);