@fusion.dev/helix-headless 0.158.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 (35) hide show
  1. package/custom-elements.json +674 -35
  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/element-events.generated.js +7 -1
  9. package/dist/lightbox/item/lightbox-item.d.ts +7 -1
  10. package/dist/lightbox/item/lightbox-item.js +15 -2
  11. package/dist/lightbox/root/lightbox-root.d.ts +8 -4
  12. package/dist/lightbox/root/lightbox-root.js +9 -1
  13. package/dist/lightbox/zoom-controller.d.ts +13 -4
  14. package/dist/lightbox/zoom-controller.js +17 -0
  15. package/dist/steps/root/steps-root.js +8 -6
  16. package/dist/steps/step/step.d.ts +9 -0
  17. package/dist/steps/step/step.js +19 -2
  18. package/dist/tabs/panel/tab-panel.d.ts +22 -1
  19. package/dist/tabs/panel/tab-panel.js +22 -1
  20. package/dist/tabs/root/tabs-root.d.ts +21 -5
  21. package/dist/tabs/root/tabs-root.js +24 -13
  22. package/dist/tabs/tab/tab.d.ts +4 -2
  23. package/dist/tabs/tab/tab.js +3 -1
  24. package/dist/toast/item/toast-item.d.ts +27 -8
  25. package/dist/toast/item/toast-item.js +47 -11
  26. package/dist/toast/root/toast-root.d.ts +24 -1
  27. package/dist/toast/root/toast-root.js +54 -0
  28. package/dist/tour/root/tour-root.d.ts +34 -0
  29. package/dist/tour/root/tour-root.js +92 -3
  30. package/dist/tour/stop/tour-stop.d.ts +33 -1
  31. package/dist/tour/stop/tour-stop.js +12 -1
  32. package/lint.json +1 -1
  33. package/package.json +5 -5
  34. package/types/jsx.d.ts +459 -62
  35. package/types/vue.d.ts +100 -14
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { auditName, CommandController, customElement, DelayController, deepActiveElement, deepContains, drawDismiss, drawDismissGlyph, FocusScopeController, FullscreenController, formatWords, HelixElement, hostAria, IdleController, inlineStep, isRtl, keyClaimed, lockScroll, MixinControllerActive, MixinControllerOpen, resolveActiveItem, SwipeController, swipeStep, unlockScroll, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { auditName, CommandController, customElement, DelayController, deepActiveElement, deepContains, drawDismiss, drawDismissGlyph, FocusScopeController, FullscreenController, formatWords, HelixElement, hostAria, IdleController, inlineStep, isRtl, keyClaimed, lockScroll, MixinControllerActive, MixinControllerOpen, OpenPhaseController, resolveActiveItem, SwipeController, swipeStep, unlockScroll, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft, caretRight, minus, plus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -63,6 +63,14 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
63
63
  ],
64
64
  surface: () => this.dialog,
65
65
  });
66
+ this.phases = new OpenPhaseController(this, {
67
+ mark: {
68
+ entering: (on) => this.toggleState('entering', on),
69
+ exiting: (on) => this.toggleState('exiting', on),
70
+ },
71
+ open: () => this.open,
72
+ surface: () => this.dialog,
73
+ });
66
74
  this.zoomer = new ZoomController(this, {
67
75
  max: () => (this.open ? this.maxZoom : 1),
68
76
  onChange: (scale, pan) => this.drawZoom(scale, pan),
@@ -12,10 +12,12 @@ export interface ZoomOptions {
12
12
  * Pinch, double press and drag to zoom into the item a viewer shows, and to
13
13
  * move about it once zoomed.
14
14
  *
15
- * Two fingers scale the item about the point between them. A double press, a
16
- * double click or a double tap, zooms all the way in about the point pressed,
17
- * and back out again. One pointer dragged across a zoomed item moves it, never
18
- * so far that an edge comes inside the box. What it reports is a scale and an
15
+ * Two fingers scale the item about the point between them. A trackpad pinch,
16
+ * which the browser sends as a wheel with Ctrl held, and the wheel with Ctrl
17
+ * held scale it about the pointer. A double press, a double click or a double
18
+ * tap, zooms all the way in about the point pressed, and back out again. One
19
+ * pointer dragged across a zoomed item moves it, never so far that an edge
20
+ * comes inside the box, and so does the wheel without Ctrl. What it reports is a scale and an
19
21
  * offset from the middle of the box; drawing them is the caller's.
20
22
  */
21
23
  export declare class ZoomController implements ReactiveController {
@@ -58,6 +60,13 @@ export declare class ZoomController implements ReactiveController {
58
60
  private readonly handleDown;
59
61
  private readonly handleMove;
60
62
  private readonly handleUp;
63
+ /**
64
+ * A wheel with Ctrl held, which is also how the browser sends a trackpad
65
+ * pinch, scales the item about the pointer. Without Ctrl, a wheel over a
66
+ * zoomed item moves it. Over an item at its own size, a plain wheel is left
67
+ * to the page.
68
+ */
69
+ private readonly handleWheel;
61
70
  /** A double press zooms all the way in about the point, or back out. */
62
71
  private readonly handleDouble;
63
72
  private release;
@@ -48,6 +48,21 @@ export class ZoomController {
48
48
  }
49
49
  this.begin();
50
50
  };
51
+ this.handleWheel = (event) => {
52
+ if (this.options.max() <= 1 || !this.inside(event)) {
53
+ return;
54
+ }
55
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 400 : 1;
56
+ if (event.ctrlKey) {
57
+ event.preventDefault();
58
+ this.zoomTo(this.scale * Math.exp((-event.deltaY * unit) / 250), this.offset(event));
59
+ return;
60
+ }
61
+ if (this.scale > 1) {
62
+ event.preventDefault();
63
+ this.panBy({ x: -event.deltaX * unit, y: -event.deltaY * unit });
64
+ }
65
+ };
51
66
  this.handleDouble = (event) => {
52
67
  if (this.options.max() <= 1 || !this.inside(event)) {
53
68
  return;
@@ -69,10 +84,12 @@ export class ZoomController {
69
84
  hostConnected() {
70
85
  this.host.addEventListener('pointerdown', this.handleDown);
71
86
  this.host.addEventListener('dblclick', this.handleDouble);
87
+ this.host.addEventListener('wheel', this.handleWheel, { passive: false });
72
88
  }
73
89
  hostDisconnected() {
74
90
  this.host.removeEventListener('pointerdown', this.handleDown);
75
91
  this.host.removeEventListener('dblclick', this.handleDouble);
92
+ this.host.removeEventListener('wheel', this.handleWheel);
76
93
  this.release();
77
94
  }
78
95
  zoomTo(scale, at = { x: 0, y: 0 }) {
@@ -34,14 +34,16 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
34
34
  wrap: false,
35
35
  });
36
36
  this.handleSelection = (event) => {
37
- if (event.defaultPrevented || !this.steps.includes(event.target)) {
37
+ if (!this.steps.includes(event.target)) {
38
38
  return;
39
39
  }
40
- const index = this.steps.indexOf(event.target);
41
- if (!this.reachable(index) || !this.valid(index)) {
42
- return;
43
- }
44
- this.requestActive(event.detail.value);
40
+ queueMicrotask(() => {
41
+ const index = this.steps.indexOf(event.target);
42
+ if (event.defaultPrevented || !this.reachable(index) || !this.valid(index)) {
43
+ return;
44
+ }
45
+ this.requestActive(event.detail.value);
46
+ });
45
47
  };
46
48
  this.handleSlotChange = () => {
47
49
  this.sync();
@@ -27,6 +27,10 @@ import '../../button/index.js';
27
27
  * region. The current stage's button carries `aria-current="step"`, and only
28
28
  * the button, so the state is said once, on the control.
29
29
  *
30
+ * A stage's `content`, while it holds anything, is a group named by the
31
+ * stage's name, so a screen reader moving into it hears which stage it
32
+ * belongs to.
33
+ *
30
34
  * @since 0.1.0
31
35
  * @status stable
32
36
  * @aliases Stage
@@ -105,6 +109,11 @@ export declare class HelixHeadlessStep extends HelixElement {
105
109
  * The value the step sends in `helix-select`.
106
110
  */
107
111
  value: string;
112
+ /** Whether anything is slotted into `content`, which makes it a named group. */
113
+ private holding;
114
+ /** Reads whether the `content` slot holds anything. */
115
+ private readonly readContent;
116
+ protected firstUpdated(changed: PropertyValues): void;
108
117
  /** Says a stage was finished, from the first time one is. */
109
118
  private readonly status;
110
119
  /**
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, drawStatus, formatWords, HelixElement, HelixSelectEvent, hostAria, StatusController, slottedWords, words, } from '@fusion.dev/helix-core';
8
8
  import { check, circle, record } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
- import { property } from 'lit/decorators.js';
10
+ import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import '../../button/index.js';
@@ -23,9 +23,18 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
23
23
  this.locked = false;
24
24
  this.optional = false;
25
25
  this.value = '';
26
+ this.holding = false;
27
+ this.readContent = () => {
28
+ const slot = this.renderRoot.querySelector('slot[name="content"]');
29
+ this.holding = (slot?.assignedNodes() ?? []).some((node) => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? '').trim() !== '');
30
+ };
26
31
  this.status = new StatusController(this);
27
32
  }
28
33
  static { this.words = 'helix-step'; }
34
+ firstUpdated(changed) {
35
+ super.firstUpdated(changed);
36
+ this.readContent();
37
+ }
29
38
  focus(options) {
30
39
  this.partNamed('label')?.focus(options);
31
40
  }
@@ -60,6 +69,7 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
60
69
  }
61
70
  renderLabel() {
62
71
  return html `<${this.tag('button')}
72
+ id="label"
63
73
  part="label"
64
74
  exportparts="button: label-button, live: label-live, spinner: label-spinner, text: label-text"
65
75
  emphasis="ghost"
@@ -79,7 +89,11 @@ let HelixHeadlessStep = class HelixHeadlessStep extends HelixElement {
79
89
  ${this.renderLabel()}
80
90
  <span part="description"><slot name="description"></slot></span>
81
91
  </span>
82
- <div part="content"><slot name="content"></slot></div>${drawStatus(this.status)}`;
92
+ <div
93
+ part="content"
94
+ role=${this.holding ? 'group' : nothing}
95
+ aria-labelledby=${this.holding ? 'label' : nothing}
96
+ ><slot name="content" @slotchange=${this.readContent}></slot></div>${drawStatus(this.status)}`;
83
97
  }
84
98
  };
85
99
  __decorate([
@@ -111,6 +125,9 @@ __decorate([
111
125
  __decorate([
112
126
  property()
113
127
  ], HelixHeadlessStep.prototype, "value", void 0);
128
+ __decorate([
129
+ state()
130
+ ], HelixHeadlessStep.prototype, "holding", void 0);
114
131
  HelixHeadlessStep = __decorate([
115
132
  customElement('helix-headless-step')
116
133
  ], HelixHeadlessStep);
@@ -15,15 +15,24 @@ import { type PropertyValues, type TemplateResult } from 'lit';
15
15
  * the tab list lands on the panel's first link or control where there is one,
16
16
  * and on the panel itself where there is none.
17
17
  *
18
+ * **Set `lazy` for a panel that is expensive to draw.** Its content waits in a
19
+ * `<template>` inside it, and goes into the panel the first time the panel is
20
+ * shown, so it costs nothing until a reader opens that tab. Once in, it stays.
21
+ * The panel sends `helix-load` at the same moment, for content your code
22
+ * renders itself.
23
+ *
18
24
  * @since 0.1.0
19
25
  * @status stable
20
26
  * @aliases TabPanel
21
27
  * @documentation https://fusion.dev/helix/components/tabs
22
28
  *
23
- * @slot - The panel's content.
29
+ * @slot - The panel's content. Under `lazy`, a `<template>` holding it.
30
+ *
31
+ * @event {HelixLoadEvent} helix-load - Fired the first time a `lazy` panel is shown, once its template's content is in.
24
32
  *
25
33
  * @csspart panel - The panel container, which takes the focus while it holds nothing focusable.
26
34
  *
35
+ * @cssstate loaded - Once a `lazy` panel has been shown and its content is in.
27
36
  * @cssstate selected - While its tab is the one selected.
28
37
  */
29
38
  export declare class HelixHeadlessTabPanel extends HelixElement {
@@ -40,6 +49,18 @@ export declare class HelixHeadlessTabPanel extends HelixElement {
40
49
  * the pattern names one anyway.
41
50
  */
42
51
  label: string;
52
+ /**
53
+ * Whether the panel's content waits in a `<template>` until the panel is
54
+ * first shown.
55
+ */
56
+ lazy: boolean;
57
+ /** Whether a `lazy` panel has put its content in. */
58
+ private loaded;
59
+ /**
60
+ * Puts a `lazy` panel's content in the first time the panel is shown: the
61
+ * content of each `<template>` it holds, in place of the template.
62
+ */
63
+ private load;
43
64
  /**
44
65
  * Whether this panel is the visible one.
45
66
  */
@@ -4,13 +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 { customElement, focusableElements, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { customElement, focusableElements, HelixElement, HelixLoadEvent, hostAria, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.label = '';
14
+ this.lazy = false;
15
+ this.loaded = false;
14
16
  this.selected = false;
15
17
  this.value = '';
16
18
  this.holdsStop = false;
@@ -21,6 +23,19 @@ let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
21
23
  this.holdsStop = this.selected && focusableElements(this).length > 0;
22
24
  };
23
25
  }
26
+ load() {
27
+ if (!this.lazy || !this.selected || this.loaded) {
28
+ return;
29
+ }
30
+ this.loaded = true;
31
+ for (const template of [...this.children]) {
32
+ if (template instanceof HTMLTemplateElement) {
33
+ template.replaceWith(document.importNode(template.content, true));
34
+ }
35
+ }
36
+ this.toggleState('loaded', true);
37
+ this.dispatchEvent(new HelixLoadEvent());
38
+ }
24
39
  connectedCallback() {
25
40
  super.connectedCallback();
26
41
  this.watcher?.observe(this, {
@@ -36,6 +51,9 @@ let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
36
51
  }
37
52
  updated(changed) {
38
53
  super.updated(changed);
54
+ if (changed.has('selected') || changed.has('lazy')) {
55
+ this.load();
56
+ }
39
57
  if (changed.has('selected')) {
40
58
  this.readStops();
41
59
  }
@@ -58,6 +76,9 @@ let HelixHeadlessTabPanel = class HelixHeadlessTabPanel extends HelixElement {
58
76
  __decorate([
59
77
  property({ attribute: false })
60
78
  ], HelixHeadlessTabPanel.prototype, "label", void 0);
79
+ __decorate([
80
+ property({ type: Boolean })
81
+ ], HelixHeadlessTabPanel.prototype, "lazy", void 0);
61
82
  __decorate([
62
83
  property({ type: Boolean })
63
84
  ], HelixHeadlessTabPanel.prototype, "selected", void 0);
@@ -46,9 +46,9 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
46
46
  * | Enter, Space | Selects the tab with focus, or follows its link. |
47
47
  * | Tab | Leaves the list for the selected panel. |
48
48
  *
49
- * Removing the selected tab selects the first enabled tab that is left, and
50
- * `active` says so, with `helix-activate`. Where the removed tab had focus,
51
- * focus moves to the tab now selected.
49
+ * Removing the selected tab selects the enabled tab beside it, the next one
50
+ * or else the one before, and `active` says so, with `helix-activate`. Where
51
+ * the removed tab had focus, focus moves to the tab now selected.
52
52
  *
53
53
  * **More tabs than fit.** With `scroll-controls`, a strip that scrolls shows a
54
54
  * button at each end it can scroll toward, as Web Awesome's tab group does,
@@ -74,6 +74,8 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
74
74
  * @slot - The tabs, as direct children of this element.
75
75
  * @slot indicator - A mark of your own, drawn inside the `indicator` part, where `--indicator-x`, `--indicator-y`, `--indicator-inline-size` and `--indicator-block-size` reach it.
76
76
  * @slot panels - The panels, as direct children of this element. Panels inside a wrapper are not paired with tabs, because pairing reads only the direct children.
77
+ * @slot scroll-end-icon - Replaces the glyph on the button that scrolls the strip toward its end.
78
+ * @slot scroll-start-icon - Replaces the glyph on the button that scrolls the strip toward its start.
77
79
  *
78
80
  * @event {HelixActivateEvent} helix-activate - Fired after the active tab changes, so the element already holds the new value.
79
81
  * @event {HelixBeforeActivateEvent} helix-before-activate - Fired before the active tab changes, and cancelable. Canceling it is the only way to block a change.
@@ -85,7 +87,9 @@ declare const HelixHeadlessTabs_base: (new (...args: any[]) => import("@fusion.d
85
87
  * and animate yourself.
86
88
  * @csspart panels - The panel container.
87
89
  * @csspart scroll-end - The button that scrolls the strip toward its end, shown under `scroll-controls` while there is more that way.
90
+ * @csspart scroll-end-mark - The glyph on that button, where none is slotted.
88
91
  * @csspart scroll-start - The button that scrolls the strip toward its start, shown under `scroll-controls` while there is more that way.
92
+ * @csspart scroll-start-mark - The glyph on that button, where none is slotted.
89
93
  * @csspart tabs - The tab list.
90
94
  */
91
95
  export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
@@ -169,6 +173,12 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
169
173
  private scrollStrip;
170
174
  /** The values of the tabs the last pairing read, to tell a removed tab from one not yet here. */
171
175
  private held;
176
+ /**
177
+ * The enabled tab nearest to where a removed one stood: the next ones
178
+ * nearest first, then the ones before it nearest first. The same order a
179
+ * dismissed tab hands focus in.
180
+ */
181
+ private beside;
172
182
  /** Pushes the current value down to whichever tab and panel own it. */
173
183
  private sync;
174
184
  /** The tab that last had focus, so its removal can hand focus on. */
@@ -205,10 +215,16 @@ export declare class HelixHeadlessTabs extends HelixHeadlessTabs_base {
205
215
  private readonly handleSlotChange;
206
216
  protected updated(changed: PropertyValues): void;
207
217
  /**
208
- * One of the two scroll buttons, pointing the way it scrolls. A caret along
209
- * the line is mirrored on a right-to-left page, and one down a column is not.
218
+ * One of the two scroll buttons, with the glyph pointing the way it scrolls.
210
219
  */
211
220
  private renderScroll;
221
+ /**
222
+ * The glyph on a scroll button, behind a seam: slot fallback, so a consumer
223
+ * replaces it by slotting, and a method, so a tier above replaces it by
224
+ * overriding. A caret along the line is mirrored on a right-to-left page, and
225
+ * one down a column is not.
226
+ */
227
+ protected renderScrollIcon(toward: 1 | -1): TemplateResult;
212
228
  protected render(): TemplateResult;
213
229
  }
214
230
  declare global {
@@ -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);