@fusion.dev/helix-headless 0.156.0 → 0.157.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.
@@ -38,8 +38,12 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
38
38
  holdWhileHidden: true,
39
39
  paused: (paused) => {
40
40
  this.toggleAttribute('data-paused', paused);
41
+ this.toggleState('paused', paused);
41
42
  },
42
43
  });
44
+ this.waiting = false;
45
+ this.started = false;
46
+ this.holding = false;
43
47
  this.openState = undefined;
44
48
  this.handlePause = () => {
45
49
  this.countdown.hold();
@@ -47,6 +51,15 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
47
51
  this.handleResume = () => {
48
52
  this.countdown.release();
49
53
  };
54
+ this.handleKeydown = (event) => {
55
+ if (event.key !== 'Escape' || event.defaultPrevented || !this.open) {
56
+ return;
57
+ }
58
+ if (this.dismiss()) {
59
+ event.preventDefault();
60
+ event.stopPropagation();
61
+ }
62
+ };
50
63
  this.name = () => {
51
64
  const words = this.partNamed('content');
52
65
  if (!words) {
@@ -72,6 +85,23 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
72
85
  const physical = isRtl(this) ? -travel : travel;
73
86
  this.partNamed('item')?.style.setProperty('--toast-swipe-x', travel === 0 ? '' : `${Math.round(physical)}px`);
74
87
  }
88
+ wait(waiting) {
89
+ if (waiting) {
90
+ if (this.started && !this.holding) {
91
+ this.holding = true;
92
+ this.countdown.hold();
93
+ }
94
+ return;
95
+ }
96
+ if (this.holding) {
97
+ this.holding = false;
98
+ this.countdown.release();
99
+ }
100
+ else if (!this.started && this.hasUpdated) {
101
+ this.started = true;
102
+ this.countdown.start(this.duration);
103
+ }
104
+ }
75
105
  dismiss() {
76
106
  if (!this.open) {
77
107
  return true;
@@ -97,19 +127,35 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
97
127
  connectedCallback() {
98
128
  super.connectedCallback();
99
129
  if (this.hasUpdated && this.open) {
100
- this.resume();
130
+ if (this.waiting) {
131
+ this.wait(true);
132
+ }
133
+ else {
134
+ this.resume();
135
+ }
101
136
  }
137
+ this.addEventListener('keydown', this.handleKeydown);
138
+ }
139
+ disconnectedCallback() {
140
+ super.disconnectedCallback();
141
+ this.holding = false;
142
+ this.removeEventListener('keydown', this.handleKeydown);
102
143
  }
103
144
  willUpdate(changed) {
104
145
  super.willUpdate(changed);
105
146
  this.toggleAttribute('data-closed', !this.open);
106
- this.toggleAttribute('hidden', !this.open);
147
+ this.toggleAttribute('hidden', !this.open || this.waiting);
148
+ this.toggleState('closed', !this.open);
149
+ this.toggleState('waiting', this.waiting);
107
150
  hostAria(this.internals, { role: 'alertdialog' });
108
151
  this.internals.ariaModal = 'false';
109
152
  }
110
153
  updated(changed) {
111
154
  super.updated(changed);
112
155
  this.name();
156
+ if (changed.has('waiting')) {
157
+ this.wait(this.waiting);
158
+ }
113
159
  }
114
160
  assigned(name) {
115
161
  return (this.renderRoot
@@ -118,7 +164,10 @@ let HelixHeadlessToastItem = class HelixHeadlessToastItem extends MixinDismissib
118
164
  }
119
165
  firstUpdated(changed) {
120
166
  super.firstUpdated(changed);
121
- this.countdown.start(this.duration);
167
+ if (!this.waiting) {
168
+ this.started = true;
169
+ this.countdown.start(this.duration);
170
+ }
122
171
  }
123
172
  renderIcon(mark) {
124
173
  return html `<slot name="icon">
@@ -183,6 +232,9 @@ __decorate([
183
232
  __decorate([
184
233
  property({ reflect: true })
185
234
  ], HelixHeadlessToastItem.prototype, "tone", void 0);
235
+ __decorate([
236
+ property({ attribute: false, type: Boolean })
237
+ ], HelixHeadlessToastItem.prototype, "waiting", void 0);
186
238
  HelixHeadlessToastItem = __decorate([
187
239
  customElement('helix-headless-toast-item')
188
240
  ], HelixHeadlessToastItem);
@@ -49,6 +49,19 @@ import { type PropertyValues, type TemplateResult } from 'lit';
49
49
  * message, then the one before it, then back to where it was before it entered
50
50
  * the stack.
51
51
  *
52
+ * **`max-visible` limits how many messages show at once**, as Zag's `max`
53
+ * does. The messages after that many, in document order, wait: each is hidden,
54
+ * its timer has not started, and the region says it once it shows, when a
55
+ * message before it goes. A waiting message has `:state(waiting)`.
56
+ *
57
+ * **Keyboard.**
58
+ *
59
+ * | Key | What it does |
60
+ * |---|---|
61
+ * | F8, or your `hotkey` | Moves focus to the first control in the stack, and back to where it was when pressed in the stack. |
62
+ * | Tab, Shift+Tab | Moves between the controls of the messages. |
63
+ * | Escape | Dismisses the message holding focus. Focus moves to the next message, then the one before it, then back to where it was. |
64
+ *
52
65
  * @since 0.1.0
53
66
  * @status stable
54
67
  * @category overlays
@@ -81,6 +94,12 @@ export declare class HelixHeadlessToast extends HelixElement {
81
94
  * the rest of your interface.
82
95
  */
83
96
  label: string;
97
+ /**
98
+ * How many messages show at once. The messages after that many wait, hidden
99
+ * and with their timers stopped, until a message before them goes. Unset,
100
+ * every message shows.
101
+ */
102
+ maxVisible?: number;
84
103
  /**
85
104
  * Which corner or edge the stack sits at.
86
105
  *
@@ -171,6 +190,18 @@ export declare class HelixHeadlessToast extends HelixElement {
171
190
  private watcher;
172
191
  /** The messages that are showing, in order. */
173
192
  private get showing();
193
+ /** The messages `max-visible` holds back. */
194
+ private readonly held;
195
+ /**
196
+ * Holds back the messages past `max-visible`, and returns the ones it has
197
+ * just let go. A message that has been dismissed takes no place in the line.
198
+ */
199
+ private queue;
200
+ /**
201
+ * Lets go of the messages past `max-visible` that now have room, raises the
202
+ * stack and says them, once each has shown.
203
+ */
204
+ private restack;
174
205
  private get region();
175
206
  /**
176
207
  * Puts the stack in the top layer while a message shows, and on top of it.
@@ -215,6 +246,7 @@ export declare class HelixHeadlessToast extends HelixElement {
215
246
  connectedCallback(): void;
216
247
  disconnectedCallback(): void;
217
248
  protected firstUpdated(changed: PropertyValues): void;
249
+ protected updated(changed: PropertyValues): void;
218
250
  protected render(): TemplateResult;
219
251
  }
220
252
  declare global {
@@ -40,6 +40,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
40
40
  }
41
41
  };
42
42
  this.made = new WeakSet();
43
+ this.held = new WeakSet();
43
44
  this.handleFocusIn = (event) => {
44
45
  const from = event.relatedTarget;
45
46
  if (from instanceof HTMLElement && !deepContains(this, from)) {
@@ -165,7 +166,8 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
165
166
  }
166
167
  sweep() {
167
168
  for (const one of [...this.children]) {
168
- if (!(one instanceof HTMLElement) || !one.hidden || !this.made.has(one)) {
169
+ const closed = one.open === false;
170
+ if (!(one instanceof HTMLElement) || !closed || !one.hidden || !this.made.has(one)) {
169
171
  continue;
170
172
  }
171
173
  this.made.delete(one);
@@ -173,7 +175,38 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
173
175
  }
174
176
  }
175
177
  get showing() {
176
- return [...this.children].filter((one) => one instanceof HTMLElement && !one.hidden);
178
+ return [...this.children].filter((one) => one instanceof HTMLElement && !one.hidden && !this.held.has(one));
179
+ }
180
+ queue() {
181
+ const max = this.maxVisible;
182
+ const line = [...this.children].filter((one) => one instanceof HTMLElement && one.open !== false);
183
+ const released = [];
184
+ line.forEach((one, at) => {
185
+ const wait = max !== undefined && max >= 0 && at >= max;
186
+ if (this.held.has(one) && !wait) {
187
+ released.push(one);
188
+ }
189
+ if (wait) {
190
+ this.held.add(one);
191
+ }
192
+ else {
193
+ this.held.delete(one);
194
+ }
195
+ if ((one.waiting ?? false) !== wait) {
196
+ one.waiting = wait;
197
+ }
198
+ });
199
+ return released;
200
+ }
201
+ restack() {
202
+ const released = this.queue();
203
+ if (released.length === 0) {
204
+ return;
205
+ }
206
+ void Promise.all(released.map((one) => one.updateComplete)).then(() => {
207
+ this.place(released);
208
+ this.tell(released);
209
+ });
177
210
  }
178
211
  get region() {
179
212
  return this.partNamed('region');
@@ -238,6 +271,7 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
238
271
  document.addEventListener('keydown', this.handleHotkey);
239
272
  this.watcher = new MutationObserver((records) => {
240
273
  const added = records.flatMap((record) => [...record.addedNodes]);
274
+ this.restack();
241
275
  this.place(added);
242
276
  this.tell(added);
243
277
  this.sweep();
@@ -262,8 +296,16 @@ let HelixHeadlessToast = class HelixHeadlessToast extends HelixElement {
262
296
  }
263
297
  firstUpdated(changed) {
264
298
  super.firstUpdated(changed);
299
+ this.queue();
265
300
  this.place();
266
301
  }
302
+ updated(changed) {
303
+ super.updated(changed);
304
+ if (changed.has('maxVisible')) {
305
+ this.restack();
306
+ this.place();
307
+ }
308
+ }
267
309
  render() {
268
310
  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>`;
269
311
  }
@@ -275,6 +317,9 @@ __decorate([
275
317
  words,
276
318
  property()
277
319
  ], HelixHeadlessToast.prototype, "label", void 0);
320
+ __decorate([
321
+ property({ attribute: 'max-visible', type: Number })
322
+ ], HelixHeadlessToast.prototype, "maxVisible", void 0);
278
323
  __decorate([
279
324
  property({ reflect: true })
280
325
  ], HelixHeadlessToast.prototype, "placement", void 0);
@@ -129,6 +129,7 @@ import '../../menu/index.js';
129
129
  * @csspart entry-shortcut-key - One key of a row's shortcut chord, inside `entry-shortcut`. The player's rows set none.
130
130
  * @csspart entry-shortcut-separator - The `+` between two keys of a row's shortcut, where the platform writes one.
131
131
  * @csspart entry-start - The leading column that holds the `start` slot. It is always rendered.
132
+ * @csspart entry-surface - The panel a row's submenu opens in beside it. The player's rows have none.
132
133
  * @csspart failure - The message shown when the source cannot be played.
133
134
  * @csspart forward - The button that skips forward ten seconds.
134
135
  * @csspart fullscreen - The button that enters and leaves fullscreen.
@@ -610,7 +610,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
610
610
  part="entry"
611
611
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
612
612
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
613
- shortcut-separator: entry-shortcut-separator, start: entry-start"
613
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
614
614
  value="off"
615
615
  checkable
616
616
  exclusive
@@ -623,7 +623,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
623
623
  ${tracks.map((track, at) => html `<${this.tag('menu-item')} part="entry"
624
624
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
625
625
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
626
- shortcut-separator: entry-shortcut-separator, start: entry-start"
626
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
627
627
  value=${track.language || String(at)}
628
628
  checkable
629
629
  exclusive
@@ -652,7 +652,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
652
652
  return html `<${this.tag('menu-item')} part="entry"
653
653
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
654
654
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
655
- shortcut-separator: entry-shortcut-separator, start: entry-start"
655
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
656
656
  value="remote"
657
657
  @helix-select=${() => void this.remote.prompt()}
658
658
  >
@@ -664,7 +664,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
664
664
  return html `<${this.tag('menu-item')} part="entry"
665
665
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
666
666
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
667
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="speed" @helix-select=${() => this.cycleSpeed()}>
667
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="speed" @helix-select=${() => this.cycleSpeed()}>
668
668
  <slot name="speed-icon" slot="start">${drawIcon(gauge)}</slot>
669
669
  ${this.speedLabel}: ${this.rate}x
670
670
  </${this.tag('menu-item')}>`;
@@ -674,7 +674,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
674
674
  return html `<${this.tag('menu-item')} part="entry"
675
675
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
676
676
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
677
- shortcut-separator: entry-shortcut-separator, start: entry-start"
677
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
678
678
  value=${one}
679
679
  @helix-select=${() => this.skipBy(back ? -SKIP_SECONDS : SKIP_SECONDS)}
680
680
  >
@@ -688,7 +688,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
688
688
  return html `<${this.tag('menu-item')} part="entry"
689
689
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
690
690
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
691
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="previous" @helix-select=${() => this.skip(true)}>
691
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="previous" @helix-select=${() => this.skip(true)}>
692
692
  <slot name="previous-icon" slot="start">${drawIcon(skipBack)}</slot>
693
693
  ${this.previousLabel}
694
694
  </${this.tag('menu-item')}>`;
@@ -697,7 +697,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
697
697
  return html `<${this.tag('menu-item')} part="entry"
698
698
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
699
699
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
700
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="next" @helix-select=${() => this.skip(false)}>
700
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="next" @helix-select=${() => this.skip(false)}>
701
701
  <slot name="next-icon" slot="start">${drawIcon(skipForward)}</slot>
702
702
  ${this.nextLabel}
703
703
  </${this.tag('menu-item')}>`;
@@ -707,7 +707,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
707
707
  return html `<${this.tag('menu-item')} part="entry"
708
708
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
709
709
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
710
- shortcut-separator: entry-shortcut-separator, start: entry-start"
710
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
711
711
  ?download=${local}
712
712
  href=${this.src}
713
713
  rel=${local ? nothing : 'noreferrer'}
@@ -721,7 +721,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
721
721
  return html `<${this.tag('menu-item')} part="entry"
722
722
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
723
723
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
724
- shortcut-separator: entry-shortcut-separator, start: entry-start" value="pip" @helix-select=${() => void this.togglePip()}>
724
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface" value="pip" @helix-select=${() => void this.togglePip()}>
725
725
  <slot name="pip-icon" slot="start">${drawIcon(pictureInPicture)}</slot>
726
726
  ${this.pipLabel}
727
727
  </${this.tag('menu-item')}>`;
@@ -30,6 +30,7 @@ import '../../menu/index.js';
30
30
  * @csspart entry-shortcut-key - One key of an entry's shortcut chord, inside `entry-shortcut`. The playlist's entries set none.
31
31
  * @csspart entry-shortcut-separator - The `+` between two keys of an entry's shortcut, where the platform writes one.
32
32
  * @csspart entry-start - The column at the start of an entry. It is a slot and is always present.
33
+ * @csspart entry-surface - The panel an entry's submenu opens in beside it. The playlist's entries have none.
33
34
  * @csspart menu - The menu that holds the entries.
34
35
  * @csspart menu-back - The button that goes back from a submenu.
35
36
  * @csspart menu-back-mark - The icon on a submenu's back button.
@@ -88,7 +88,7 @@ let HelixHeadlessVideoPlaylistMenu = class HelixHeadlessVideoPlaylistMenu extend
88
88
  part="entry"
89
89
  exportparts="end: entry-end, item: entry-item, mark: entry-mark, shortcut: entry-shortcut,
90
90
  shortcut-kbd: entry-shortcut-kbd, shortcut-key: entry-shortcut-key,
91
- shortcut-separator: entry-shortcut-separator, start: entry-start"
91
+ shortcut-separator: entry-shortcut-separator, start: entry-start, surface: entry-surface"
92
92
  value=${entry.value}
93
93
  checkable
94
94
  ?checked=${entry.current}