@fusion.dev/helix-headless 0.158.0 → 0.160.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 (58) hide show
  1. package/custom-elements.json +1925 -174
  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/date-input/root/date-input-root.d.ts +108 -19
  9. package/dist/date-input/root/date-input-root.js +112 -13
  10. package/dist/date-picker/root/date-picker-root.d.ts +81 -16
  11. package/dist/date-picker/root/date-picker-root.js +140 -36
  12. package/dist/element-events.generated.js +9 -1
  13. package/dist/format-date/fields.js +1 -0
  14. package/dist/format-date/options.d.ts +3 -1
  15. package/dist/format-date/options.js +4 -1
  16. package/dist/format-date/root/format-date-root.d.ts +27 -11
  17. package/dist/format-date/root/format-date-root.js +6 -0
  18. package/dist/lightbox/item/lightbox-item.d.ts +7 -1
  19. package/dist/lightbox/item/lightbox-item.js +15 -2
  20. package/dist/lightbox/root/lightbox-root.d.ts +8 -4
  21. package/dist/lightbox/root/lightbox-root.js +9 -1
  22. package/dist/lightbox/zoom-controller.d.ts +13 -4
  23. package/dist/lightbox/zoom-controller.js +17 -0
  24. package/dist/listbox/root/listbox-root.js +5 -0
  25. package/dist/notifications/notification/notification.d.ts +1 -0
  26. package/dist/notifications/notification/notification.js +2 -1
  27. package/dist/slider/root/slider-root.d.ts +54 -3
  28. package/dist/slider/root/slider-root.js +124 -2
  29. package/dist/steps/root/steps-root.js +8 -6
  30. package/dist/steps/step/step.d.ts +9 -0
  31. package/dist/steps/step/step.js +19 -2
  32. package/dist/tabs/panel/tab-panel.d.ts +22 -1
  33. package/dist/tabs/panel/tab-panel.js +22 -1
  34. package/dist/tabs/root/tabs-root.d.ts +21 -5
  35. package/dist/tabs/root/tabs-root.js +24 -13
  36. package/dist/tabs/tab/tab.d.ts +4 -2
  37. package/dist/tabs/tab/tab.js +3 -1
  38. package/dist/time-input/root/time-input-root.d.ts +55 -7
  39. package/dist/time-input/root/time-input-root.js +70 -10
  40. package/dist/time-picker/root/time-picker-root.d.ts +27 -5
  41. package/dist/time-picker/root/time-picker-root.js +52 -13
  42. package/dist/toast/item/toast-item.d.ts +27 -8
  43. package/dist/toast/item/toast-item.js +47 -11
  44. package/dist/toast/root/toast-root.d.ts +24 -1
  45. package/dist/toast/root/toast-root.js +54 -0
  46. package/dist/toggle/group/toggle-group.d.ts +18 -1
  47. package/dist/toggle/group/toggle-group.js +8 -2
  48. package/dist/toggle/root/toggle-root.d.ts +2 -2
  49. package/dist/toggle/root/toggle-root.js +2 -1
  50. package/dist/tour/root/tour-root.d.ts +34 -0
  51. package/dist/tour/root/tour-root.js +92 -3
  52. package/dist/tour/stop/tour-stop.d.ts +33 -1
  53. package/dist/tour/stop/tour-stop.js +12 -1
  54. package/lint.json +1 -1
  55. package/package.json +5 -5
  56. package/types/jsx.d.ts +1119 -218
  57. package/types/vue.d.ts +278 -51
  58. package/types/words.d.ts +72 -0
@@ -4,8 +4,8 @@ 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, valueAsTime as asTime, clockLocale, customElement, formatWords, timeAsValue as fromTime, HelixFormElement, handedLocale, isClockValue, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerEditing, MixinPickerField, MixinSegmentedField, MixinTokens, timeFault, words, wordsLocale, writeClock, } from '@fusion.dev/helix-core';
8
- import { clock } from '@fusion.dev/helix-icons';
7
+ import { AriaForwardController, valueAsTime as asTime, clockLocale, customElement, drawIcon, formatWords, timeAsValue as fromTime, HelixClearEvent, HelixFormElement, handedLocale, isClockValue, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerEditing, MixinPickerField, MixinSegmentedField, MixinTokens, readWrittenTime, timeFault, words, wordsLocale, writeClock, } from '@fusion.dev/helix-core';
8
+ import { clock, x } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -19,6 +19,8 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
19
19
  target: () => this.partNamed('control') ?? this.segmentGroup,
20
20
  });
21
21
  this.autocomplete = '';
22
+ this.clearable = false;
23
+ this.clearLabel = 'Clear';
22
24
  this.announcesInput = true;
23
25
  this.disableTimes = '';
24
26
  this.locale = '';
@@ -34,16 +36,28 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
34
36
  this.timeStyle = 'local';
35
37
  this.triggerLabel = 'Choose a time';
36
38
  this.unavailableMessage = 'Choose a time that is available.';
39
+ this.unreadableMessage = '{mode, select, single {Enter a time as {shape}.} other {Enter times as {shape}, separated by commas.}}';
37
40
  this.draft = '';
41
+ this.handleTextCommit = (event) => {
42
+ if (this.mode !== 'multiple') {
43
+ const held = this.held;
44
+ const read = held.map((one) => this.read(one));
45
+ if (held.length > 0 && !held.every((one) => this.isTime(one)) && read.every(Boolean)) {
46
+ this.requestValue(read.join(','));
47
+ }
48
+ }
49
+ this.handleCommit(event);
50
+ };
38
51
  this.handleDraftKey = (event) => {
39
52
  if (this.mode !== 'multiple') {
40
53
  return;
41
54
  }
42
55
  const held = this.held;
43
- if (event.key === 'Enter' && this.isTime(this.draft)) {
56
+ const draft = this.read(this.draft.trim());
57
+ if (event.key === 'Enter' && draft !== '') {
44
58
  event.preventDefault();
45
- if (!held.includes(this.draft)) {
46
- this.requestPicked([...held, this.draft].sort().join(','));
59
+ if (!held.includes(draft)) {
60
+ this.requestPicked([...held, draft].sort().join(','));
47
61
  }
48
62
  this.draft = '';
49
63
  }
@@ -91,6 +105,13 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
91
105
  this.returnFocus();
92
106
  }
93
107
  };
108
+ this.handleClear = () => {
109
+ if (this.value !== '' && this.requestValue('')) {
110
+ this.announceChange();
111
+ this.dispatchEvent(new HelixClearEvent());
112
+ }
113
+ void this.updateComplete.then(() => this.returnFocus());
114
+ };
94
115
  }
95
116
  static { this.words = 'helix-time-input'; }
96
117
  get language() {
@@ -104,7 +125,10 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
104
125
  .filter(Boolean);
105
126
  }
106
127
  get segmentable() {
107
- return this.mode === undefined;
128
+ return this.mode !== 'multiple';
129
+ }
130
+ get segmentGroups() {
131
+ return this.mode === 'range' ? 2 : 1;
108
132
  }
109
133
  get segmentedKind() {
110
134
  return 'time';
@@ -141,6 +165,9 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
141
165
  get restCount() {
142
166
  return Math.max(0, this.held.length - this.shownTimes.length);
143
167
  }
168
+ read(text) {
169
+ return this.isTime(text) ? text : readWrittenTime(text, this.language, this.seconds);
170
+ }
144
171
  get picked() {
145
172
  const held = this.held;
146
173
  return held.length > 0 && held.every((one) => this.isTime(one)) ? held.join(',') : '';
@@ -157,9 +184,7 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
157
184
  const shape = this.seconds ? 'HH:mm:ss' : 'HH:mm';
158
185
  if (held.length > 0 && !held.every((one) => this.isTime(one))) {
159
186
  return (this.badInputMessage ||
160
- (this.mode === undefined
161
- ? `Enter a time as ${shape}.`
162
- : `Enter times as ${shape}, separated by commas.`));
187
+ formatWords(this, 'unreadableMessage', { mode: this.mode ?? 'single', shape }));
163
188
  }
164
189
  const rules = { disableTimes: this.disableTimes, max: this.max, min: this.min };
165
190
  const faults = held.map((one) => timeFault(one, rules));
@@ -249,6 +274,12 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
249
274
  ${unsafeSVG(clock.content)}
250
275
  </svg></slot>`;
251
276
  }
277
+ get showsClear() {
278
+ return this.clearable && this.value !== '' && !this.isDisabled && !this.readOnly;
279
+ }
280
+ renderClearIcon() {
281
+ return html `<slot name="clear-icon">${drawIcon(x)}</slot>`;
282
+ }
252
283
  renderControl() {
253
284
  return html `<input
254
285
  part="control"
@@ -257,6 +288,10 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
257
288
  .value=${this.display}
258
289
  name=${this.name || nothing}
259
290
  placeholder=${this.mode === 'multiple' && this.held.length > 0 ? nothing : this.placeholder || nothing}
291
+ role="combobox"
292
+ aria-controls="surface"
293
+ aria-expanded=${this.open ? 'true' : 'false'}
294
+ aria-haspopup="dialog"
260
295
  autocomplete=${this.autocomplete || nothing}
261
296
  aria-describedby=${this.describedBy ?? nothing}
262
297
  aria-invalid=${this.invalidity ?? nothing}
@@ -264,13 +299,17 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
264
299
  ?readonly=${this.readOnly}
265
300
  ?required=${this.required}
266
301
  @blur=${this.handleBlur}
267
- @change=${this.handleCommit}
302
+ @change=${this.handleTextCommit}
268
303
  @focus=${this.handleFocus}
269
304
  @input=${this.handleInput}
270
305
  @keydown=${this.handleDraftKey}
271
306
  @keypress=${this.handleKeypress}
272
307
  />`;
273
308
  }
309
+ willUpdate(changed) {
310
+ super.willUpdate(changed);
311
+ this.toggleState('blank', this.value === '');
312
+ }
274
313
  render() {
275
314
  return html `${this.renderLabel()}
276
315
  <div part="field" @keydown=${this.handleKeydown}>
@@ -280,6 +319,16 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
280
319
  : nothing}
281
320
  ${this.segmentsShown ? this.renderSegments() : this.renderControl()}
282
321
  <slot name="end"></slot>
322
+ ${this.showsClear
323
+ ? html `<button
324
+ part="clear"
325
+ type="button"
326
+ aria-label=${this.clearLabel}
327
+ @click=${this.handleClear}
328
+ >
329
+ ${this.renderClearIcon()}
330
+ </button>`
331
+ : nothing}
283
332
  <button
284
333
  part="trigger"
285
334
  type="button"
@@ -327,6 +376,13 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinSegmented
327
376
  __decorate([
328
377
  property()
329
378
  ], HelixHeadlessTimeInput.prototype, "autocomplete", void 0);
379
+ __decorate([
380
+ property({ type: Boolean })
381
+ ], HelixHeadlessTimeInput.prototype, "clearable", void 0);
382
+ __decorate([
383
+ words,
384
+ property({ attribute: 'clear-label' })
385
+ ], HelixHeadlessTimeInput.prototype, "clearLabel", void 0);
330
386
  __decorate([
331
387
  property({ attribute: 'disable-times' })
332
388
  ], HelixHeadlessTimeInput.prototype, "disableTimes", void 0);
@@ -382,6 +438,10 @@ __decorate([
382
438
  words,
383
439
  property({ attribute: 'unavailable-message' })
384
440
  ], HelixHeadlessTimeInput.prototype, "unavailableMessage", void 0);
441
+ __decorate([
442
+ words,
443
+ property({ attribute: 'unreadable-message' })
444
+ ], HelixHeadlessTimeInput.prototype, "unreadableMessage", void 0);
385
445
  __decorate([
386
446
  state()
387
447
  ], HelixHeadlessTimeInput.prototype, "draft", void 0);
@@ -1,4 +1,4 @@
1
- import { HelixFormElement, type HourCycle } from '@fusion.dev/helix-core';
1
+ import { HelixFormElement, type HourCycle, type PickerFailure } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
4
  import '../../option/index.js';
@@ -51,7 +51,10 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
51
51
  * host's `aria-label` or its `aria-labelledby`. A development build warns
52
52
  * where all four are empty. The list is described by the hint, or by the
53
53
  * message in `feedback` in its place, and carries `aria-multiselectable` with
54
- * a `mode` and `aria-invalid` while `feedback` says what is wrong. The wheels
54
+ * a `mode`. It carries `aria-invalid` while `feedback` says what is wrong,
55
+ * while it holds a time outside `min` and `max` or one the picker does not
56
+ * offer, and, for a `required` picker with no time, once the reader has
57
+ * chosen one or a form has refused the picker. The wheels
55
58
  * are a `group` with that name, holding one `listbox` per wheel, named "Hour",
56
59
  * "Minute", "Second" and "AM/PM" in the page's language, and each described
57
60
  * the same way.
@@ -100,7 +103,7 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
100
103
  * @csspart list-loading - The text shown while the options load.
101
104
  * @csspart list-search - The region that holds the filter field.
102
105
  * @csspart list-search-field - The filter field.
103
- * @csspart option - One time in the list. It carries `data-between` inside a range.
106
+ * @csspart option - One time in the list. It carries `data-between` inside a range, and inside the run to the time under the pointer while a range waits for its second end.
104
107
  * @csspart option-between - A time inside a chosen range, excluding its ends, beside `option`.
105
108
  * @csspart option-description - The caption under the time's label, when there is one.
106
109
  * @csspart option-item - The row.
@@ -110,7 +113,8 @@ declare const HelixHeadlessTimePicker_base: (new (...args: any[]) => import("@fu
110
113
  * @csspart option-text - The label and the caption, as one column.
111
114
  *
112
115
  * @cssstate blank - While no time is chosen.
113
- * @cssstate invalid - While `invalid` is set or `feedback` says what is wrong.
116
+ * @cssstate invalid - While `invalid` is set, `feedback` says what is wrong, or the list carries `aria-invalid`.
117
+ * @cssstate range-pending - While a `range` holds its first time and waits for the second.
114
118
  */
115
119
  export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_base {
116
120
  /**
@@ -318,7 +322,18 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
318
322
  * sequence.
319
323
  */
320
324
  private readonly handleChange;
321
- /** Whether a time falls inside a range, which is what gets banded. */
325
+ /**
326
+ * The time under the pointer while a range holds one end, which the list
327
+ * bands the range to before it is chosen, as Date Picker does.
328
+ */
329
+ private previewed;
330
+ private readonly handleListHover;
331
+ private readonly handleListLeave;
332
+ /**
333
+ * Whether a time falls inside a range, which is what gets banded: the
334
+ * chosen range, or while one end waits, the run to the time under the
335
+ * pointer.
336
+ */
322
337
  private between;
323
338
  /** The readings the wheels show, from the value or from `min`. */
324
339
  private get parts();
@@ -463,6 +478,13 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
463
478
  * browser points its bubble at.
464
479
  */
465
480
  private reportRequired;
481
+ /**
482
+ * What is wrong with the answer, as validity flags and a message, or nothing.
483
+ * `reportRequired` hands it to the form, and `failure` to the mark.
484
+ */
485
+ private get problem();
486
+ /** A missing time waits for the reader; a refused one is marked at once. */
487
+ protected get failure(): PickerFailure | undefined;
466
488
  /**
467
489
  * Reports the times to the form, one entry each.
468
490
  *
@@ -53,6 +53,19 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
53
53
  const added = next.find((one) => !before.has(one));
54
54
  this.requestValue(added ?? '');
55
55
  };
56
+ this.previewed = '';
57
+ this.handleListHover = (event) => {
58
+ const option = event
59
+ .composedPath()
60
+ .find((one) => one instanceof HTMLElement && one.hasAttribute('value'));
61
+ const at = option?.getAttribute('value') ?? '';
62
+ if (this.mode === 'range' && this.held.length === 1 && at !== this.previewed) {
63
+ this.previewed = at;
64
+ }
65
+ };
66
+ this.handleListLeave = () => {
67
+ this.previewed = '';
68
+ };
56
69
  this.moveColumn = (part, to) => {
57
70
  if (this.isDisabled) {
58
71
  return;
@@ -146,8 +159,11 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
146
159
  if (this.mode !== 'range') {
147
160
  return false;
148
161
  }
149
- const [from, to] = this.held;
150
- return from !== undefined && to !== undefined && at > from && at < to;
162
+ const [from, to = this.previewed] = this.held;
163
+ if (from === undefined || to === '' || this.held.length > 2) {
164
+ return false;
165
+ }
166
+ return at > (from < to ? from : to) && at < (from < to ? to : from);
151
167
  }
152
168
  get parts() {
153
169
  const at = valueAsSeconds(this.value) ?? valueAsSeconds(this.min) ?? 0;
@@ -218,27 +234,41 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
218
234
  }
219
235
  }
220
236
  reportRequired() {
221
- const anchor = this.partNamed('list') ?? undefined;
222
- if (this.required && this.value === '') {
223
- this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage, anchor);
237
+ const problem = this.problem;
238
+ if (problem) {
239
+ this.internals.setValidity(problem[0], problem[1], this.partNamed('list') ?? undefined);
224
240
  return;
225
241
  }
242
+ this.internals.setValidity({});
243
+ }
244
+ get problem() {
245
+ if (this.required && this.value === '') {
246
+ return [{ valueMissing: true }, this.valueMissingMessage];
247
+ }
226
248
  const held = this.held;
227
249
  if (held.length > 0 && held.some((one) => valueAsSeconds(one) === null)) {
228
- this.internals.setValidity({ badInput: true }, formatWords(this, 'badInputMessage', {
229
- mode: this.mode ?? 'single',
230
- shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
231
- }), anchor);
232
- return;
250
+ return [
251
+ { badInput: true },
252
+ formatWords(this, 'badInputMessage', {
253
+ mode: this.mode ?? 'single',
254
+ shape: this.seconds ? 'HH:mm:ss' : 'HH:mm',
255
+ }),
256
+ ];
233
257
  }
234
258
  for (const one of held) {
235
259
  const fault = this.fault(one);
236
260
  if (fault) {
237
- this.internals.setValidity({ [fault[0]]: true }, fault[1], anchor);
238
- return;
261
+ return [{ [fault[0]]: true }, fault[1]];
239
262
  }
240
263
  }
241
- this.internals.setValidity({});
264
+ return undefined;
265
+ }
266
+ get failure() {
267
+ const problem = this.problem;
268
+ if (!problem) {
269
+ return undefined;
270
+ }
271
+ return problem[0].valueMissing ? 'missing' : 'refused';
242
272
  }
243
273
  report() {
244
274
  if (this.isDisabled) {
@@ -290,7 +320,11 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
290
320
  willUpdate(changed) {
291
321
  super.willUpdate(changed);
292
322
  this.report();
323
+ if (changed.has('value')) {
324
+ this.previewed = '';
325
+ }
293
326
  this.toggleState('blank', this.value === '');
327
+ this.toggleState('range-pending', this.mode === 'range' && this.held.length === 1);
294
328
  }
295
329
  firstUpdated(changed) {
296
330
  super.firstUpdated(changed);
@@ -386,6 +420,8 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
386
420
  .value=${this.value}
387
421
  @helix-before-change=${this.handleChange}
388
422
  @input=${this.handleListInput}
423
+ @pointerleave=${this.handleListLeave}
424
+ @pointerover=${this.handleListHover}
389
425
  >
390
426
  ${this.times.map((seconds) => {
391
427
  const at = this.valueFor(seconds);
@@ -481,6 +517,9 @@ __decorate([
481
517
  words,
482
518
  property({ attribute: 'range-underflow-message' })
483
519
  ], HelixHeadlessTimePicker.prototype, "rangeUnderflowMessage", void 0);
520
+ __decorate([
521
+ state()
522
+ ], HelixHeadlessTimePicker.prototype, "previewed", void 0);
484
523
  __decorate([
485
524
  property({ attribute: 'readonly', reflect: true, type: Boolean })
486
525
  ], HelixHeadlessTimePicker.prototype, "readOnly", void 0);
@@ -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
  /**