@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
@@ -5,7 +5,9 @@
5
5
  export interface Asked {
6
6
  dateStyle?: 'full' | 'long' | 'medium' | 'short' | undefined;
7
7
  day?: 'numeric' | '2-digit' | undefined;
8
+ dayPeriod?: 'long' | 'narrow' | 'short' | undefined;
8
9
  era?: 'long' | 'narrow' | 'short' | undefined;
10
+ fractionalSecondDigits?: 1 | 2 | 3 | undefined;
9
11
  hour?: 'numeric' | '2-digit' | undefined;
10
12
  hourCycle?: 'h11' | 'h12' | 'h23' | 'h24' | undefined;
11
13
  minute?: 'numeric' | '2-digit' | undefined;
@@ -13,7 +15,7 @@ export interface Asked {
13
15
  second?: 'numeric' | '2-digit' | undefined;
14
16
  timeStyle?: 'full' | 'long' | 'medium' | 'short' | undefined;
15
17
  timeZone?: string | undefined;
16
- timeZoneName?: 'long' | 'short' | undefined;
18
+ timeZoneName?: 'long' | 'longGeneric' | 'longOffset' | 'short' | 'shortGeneric' | 'shortOffset' | undefined;
17
19
  weekday?: 'long' | 'short' | 'narrow' | undefined;
18
20
  year?: 'numeric' | '2-digit' | undefined;
19
21
  }
@@ -1,5 +1,8 @@
1
1
  import { FIELD } from './fields.js';
2
- const FIELDS = Object.keys(FIELD);
2
+ const FIELDS = [
3
+ ...Object.keys(FIELD).filter((one) => one !== 'fractionalSecond'),
4
+ 'fractionalSecondDigits',
5
+ ];
3
6
  export const formatOptions = (asked, dateOnly = false) => {
4
7
  const options = {
5
8
  hourCycle: asked.hourCycle,
@@ -1,4 +1,4 @@
1
- import { type DateCalendar, HelixElement } from '@fusion.dev/helix-core';
1
+ import { type DateCalendar, HelixElement, type MonthCalendar } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A timestamp, formatted for the user's locale.
@@ -45,8 +45,8 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
45
45
  *
46
46
  * **A date is written in a calendar Date Picker draws**: the locale's own
47
47
  * where its months are the Gregorian months, as `th-TH` writes 2569 for 2026,
48
- * and `gregory` where they are not, as for `fa-IR`. `calendar` picks one of
49
- * the five.
48
+ * and `gregory` where they are not, as for `fa-IR`. `calendar` picks any of
49
+ * them, `persian` and `hebrew` included.
50
50
  *
51
51
  * @since 0.1.0
52
52
  * @status stable
@@ -57,6 +57,7 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
57
57
  * @csspart day - The day of the month, with `fields`.
58
58
  * @csspart day-period - AM or PM, or the locale's word for the part of the day, with `fields`.
59
59
  * @csspart era - The era, such as AD, with `fields` and `era`.
60
+ * @csspart fractional-second - The fraction of a second, with `fields` and `fractional-second-digits`.
60
61
  * @csspart hour - The hour, with `fields`.
61
62
  * @csspart minute - The minute, with `fields`.
62
63
  * @csspart month - The month, with `fields`.
@@ -72,14 +73,16 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
72
73
  export declare class HelixHeadlessFormatDate extends HelixElement {
73
74
  /**
74
75
  * The calendar a date is written in: `gregory`, `buddhist`, `japanese`,
75
- * `roc`, or `iso8601`, the calendars Date Picker and Date Input draw.
76
+ * `roc`, or `iso8601`, or a calendar with months of its own, such as
77
+ * `persian`, `hebrew`, `islamic-umalqura`, `ethiopic`, or `chinese`: every
78
+ * calendar Date Picker and Date Input draw.
76
79
  *
77
- * Absent is the locale's own where it is one of the five, such as
78
- * `buddhist` for `th-TH`, and `gregory` for every other, such as `persian`
79
- * for `fa-IR`, so a date written here reads as the same date a Date Picker
80
- * beside it shows.
80
+ * Absent is the locale's own where its months are the Gregorian months,
81
+ * such as `buddhist` for `th-TH`, and `gregory` for every other, such as
82
+ * `persian` for `fa-IR`, so a date written here reads as the same date a
83
+ * Date Picker beside it shows. Write `calendar="persian"` for Mehr 14, 1405.
81
84
  */
82
- calendar?: DateCalendar;
85
+ calendar?: DateCalendar | MonthCalendar;
83
86
  /**
84
87
  * The timestamp, in any format `Date` can parse.
85
88
  *
@@ -107,6 +110,12 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
107
110
  * How the day of the month is written, where a date is shown.
108
111
  */
109
112
  day?: 'numeric' | '2-digit';
113
+ /**
114
+ * Writes the part of the day in words, such as "in the morning" or "at
115
+ * night", in its `long`, `short`, or `narrow` form, where `hour` is on a
116
+ * twelve-hour clock.
117
+ */
118
+ dayPeriod?: 'long' | 'narrow' | 'short';
110
119
  /**
111
120
  * Shows how long ago the timestamp was, in a compact form such as "3h", in
112
121
  * place of the date.
@@ -130,6 +139,11 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
130
139
  * way.
131
140
  */
132
141
  fields?: boolean;
142
+ /**
143
+ * How many digits of a fraction of a second to write after `second`: 1, 2,
144
+ * or 3, as in 09:41:07.250.
145
+ */
146
+ fractionalSecondDigits?: 1 | 2 | 3;
133
147
  /**
134
148
  * Draws an `elapsed` or `relative` reading once and leaves it, where it
135
149
  * would otherwise move on as it goes stale.
@@ -206,9 +220,11 @@ export declare class HelixHeadlessFormatDate extends HelixElement {
206
220
  */
207
221
  timeZone?: string;
208
222
  /**
209
- * Shows the name of the time zone, in its `long` or `short` form.
223
+ * Shows the name of the time zone: `long` or `short`, as Pacific Daylight
224
+ * Time or PDT, `longOffset` or `shortOffset`, as GMT-07:00 or GMT-7, or
225
+ * `longGeneric` or `shortGeneric`, as Pacific Time or PT.
210
226
  */
211
- timeZoneName?: 'long' | 'short';
227
+ timeZoneName?: 'long' | 'longGeneric' | 'longOffset' | 'short' | 'shortGeneric' | 'shortOffset';
212
228
  /**
213
229
  * How the weekday is written, where one is shown.
214
230
  */
@@ -132,6 +132,9 @@ __decorate([
132
132
  __decorate([
133
133
  property()
134
134
  ], HelixHeadlessFormatDate.prototype, "day", void 0);
135
+ __decorate([
136
+ property({ attribute: 'day-period' })
137
+ ], HelixHeadlessFormatDate.prototype, "dayPeriod", void 0);
135
138
  __decorate([
136
139
  property({ reflect: true, type: Boolean })
137
140
  ], HelixHeadlessFormatDate.prototype, "elapsed", void 0);
@@ -141,6 +144,9 @@ __decorate([
141
144
  __decorate([
142
145
  property({ type: Boolean })
143
146
  ], HelixHeadlessFormatDate.prototype, "fields", void 0);
147
+ __decorate([
148
+ property({ attribute: 'fractional-second-digits', type: Number })
149
+ ], HelixHeadlessFormatDate.prototype, "fractionalSecondDigits", void 0);
144
150
  __decorate([
145
151
  property({ type: Boolean })
146
152
  ], HelixHeadlessFormatDate.prototype, "frozen", void 0);
@@ -11,7 +11,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
11
11
  * Each item is a `group` with the role description `slide`, named by its
12
12
  * `label` or, without one, by its place in the set, such as "2 of 5", as the
13
13
  * APG carousel pattern names a slide. The lightbox supplies both words, so they
14
- * are translated on the lightbox.
14
+ * are translated on the lightbox. A `description`, while there is one,
15
+ * describes the group, so a screen reader says it with the name.
15
16
  *
16
17
  * An item that is not current is hidden and stays in the DOM, as a `TabPanel`
17
18
  * does. Crawlers can read it, it is in the server-rendered markup, and an
@@ -56,6 +57,11 @@ export declare class HelixHeadlessLightboxItem extends HelixElement {
56
57
  * The item's identifier. The root compares its `active` value against it.
57
58
  */
58
59
  value: string;
60
+ /** Whether anything is slotted into `description`, which then describes the group. */
61
+ private described;
62
+ /** Reads whether the `description` slot holds anything. */
63
+ private readonly readDescription;
64
+ protected firstUpdated(changed: PropertyValues): void;
59
65
  /**
60
66
  * Mirrors the shown state where a stylesheet can select on it.
61
67
  *
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessLightboxItem = class HelixHeadlessLightboxItem extends HelixElement {
11
11
  constructor() {
12
12
  super(...arguments);
@@ -15,6 +15,15 @@ let HelixHeadlessLightboxItem = class HelixHeadlessLightboxItem extends HelixEle
15
15
  this.position = '';
16
16
  this.roleLabel = '';
17
17
  this.value = '';
18
+ this.described = false;
19
+ this.readDescription = () => {
20
+ const slot = this.renderRoot.querySelector('slot[name="description"]');
21
+ this.described = (slot?.assignedNodes() ?? []).some((node) => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? '').trim() !== '');
22
+ };
23
+ }
24
+ firstUpdated(changed) {
25
+ super.firstUpdated(changed);
26
+ this.readDescription();
18
27
  }
19
28
  willUpdate(changed) {
20
29
  super.willUpdate(changed);
@@ -26,10 +35,11 @@ let HelixHeadlessLightboxItem = class HelixHeadlessLightboxItem extends HelixEle
26
35
  role="group"
27
36
  aria-roledescription=${this.roleLabel || nothing}
28
37
  aria-label=${this.label || this.position || nothing}
38
+ aria-describedby=${this.described ? 'description' : nothing}
29
39
  ?hidden=${!this.current}
30
40
  >
31
41
  <slot></slot>
32
- <slot name="description"></slot>
42
+ <slot id="description" name="description" @slotchange=${this.readDescription}></slot>
33
43
  </div>`;
34
44
  }
35
45
  };
@@ -48,6 +58,9 @@ __decorate([
48
58
  __decorate([
49
59
  property()
50
60
  ], HelixHeadlessLightboxItem.prototype, "value", void 0);
61
+ __decorate([
62
+ state()
63
+ ], HelixHeadlessLightboxItem.prototype, "described", void 0);
51
64
  HelixHeadlessLightboxItem = __decorate([
52
65
  customElement('helix-headless-lightbox-item')
53
66
  ], HelixHeadlessLightboxItem);
@@ -1,4 +1,4 @@
1
- import { CommandController, FocusScopeController, HelixElement, SwipeController } from '@fusion.dev/helix-core';
1
+ import { CommandController, FocusScopeController, HelixElement, OpenPhaseController, SwipeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
4
4
  /**
@@ -25,9 +25,10 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
25
25
  * buttons that send `--previous`, `--next` and `--close`.
26
26
  *
27
27
  * **Set `max-zoom` to let a reader zoom into the shown item**, such as `3`.
28
- * Two fingers pinch it, a double click or a double tap zooms all the way in
29
- * about the point pressed and back out, and once zoomed one pointer drags it
30
- * about. `+` and `-` zoom in and out from the keyboard, and `0` puts it back.
28
+ * Two fingers pinch it, and so does a trackpad pinch or the wheel with Ctrl
29
+ * held, about the point under the pointer. A double click or a double tap
30
+ * zooms all the way in about the point pressed and back out, and once zoomed
31
+ * one pointer drags it about and the wheel moves it. `+` and `-` zoom in and out from the keyboard, and `0` puts it back.
31
32
  * The scale and the offset are on the `viewport` part as `--lightbox-zoom`,
32
33
  * `--lightbox-pan-x` and `--lightbox-pan-y`, written only while zoomed, and a
33
34
  * swipe moves to the next item only while the item is at its own size. With
@@ -109,6 +110,8 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
109
110
  * @cssproperty --lightbox-pan-y - How far the zoomed item is moved down from the middle of the viewport, in pixels. Written only while zoomed.
110
111
  * @cssproperty --lightbox-zoom - The scale the shown item is zoomed to, such as `2`. Written only while zoomed.
111
112
  *
113
+ * @cssstate entering - From the moment the viewer opens until its entrance has finished.
114
+ * @cssstate exiting - From the moment the viewer closes until its exit has finished.
112
115
  * @cssstate fullscreen - While the viewer fills the screen.
113
116
  * @cssstate idle - The user has been still long enough for the controls to hide.
114
117
  * @cssstate open - While the viewer is open.
@@ -225,6 +228,7 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
225
228
  private quietZoom;
226
229
  protected readonly swipes: SwipeController;
227
230
  protected readonly scope: FocusScopeController;
231
+ protected readonly phases: OpenPhaseController;
228
232
  private readonly zoomer;
229
233
  /** Writes the zoom on the viewport, and nothing at the item's own size. */
230
234
  private drawZoom;
@@ -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 }) {
@@ -98,6 +98,11 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
98
98
  }
99
99
  else if (event.key === ' ') {
100
100
  event.preventDefault();
101
+ const at = this.currentValue || shown.find((option) => !option.disabled)?.value;
102
+ if (at) {
103
+ this.activeValue = at;
104
+ this.choose(at);
105
+ }
101
106
  }
102
107
  return;
103
108
  }
@@ -56,6 +56,7 @@ declare const HelixHeadlessNotification_base: (new (...args: any[]) => import("@
56
56
  * @csspart time-day - Format Date's `day`, forwarded for a time drawn as fields. An elapsed time draws none.
57
57
  * @csspart time-day-period - Format Date's `day-period`, forwarded for a time drawn as fields. An elapsed time draws none.
58
58
  * @csspart time-era - Format Date's `era`, forwarded for a time drawn as fields. An elapsed time draws none.
59
+ * @csspart time-fractional-second - Format Date's `fractional-second`, forwarded for a time drawn as fields. An elapsed time draws none.
59
60
  * @csspart time-hour - Format Date's `hour`, forwarded for a time drawn as fields. An elapsed time draws none.
60
61
  * @csspart time-minute - Format Date's `minute`, forwarded for a time drawn as fields. An elapsed time draws none.
61
62
  * @csspart time-month - Format Date's `month`, forwarded for a time drawn as fields. An elapsed time draws none.
@@ -104,7 +104,8 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
104
104
  renderTime() {
105
105
  const stamp = this.tag('format-date');
106
106
  return html `<${stamp}
107
- exportparts="time, day: time-day, day-period: time-day-period, era: time-era, hour: time-hour,
107
+ exportparts="time, day: time-day, day-period: time-day-period, era: time-era,
108
+ fractional-second: time-fractional-second, hour: time-hour,
108
109
  minute: time-minute, month: time-month, second: time-second,
109
110
  time-zone-name: time-time-zone-name, weekday: time-weekday, year: time-year"
110
111
  elapsed
@@ -28,6 +28,10 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
28
28
  * `min-steps-between-thumbs` keeps them that many steps apart, and the readout
29
29
  * joins the two as `range-text` writes them.
30
30
  *
31
+ * A press on a range's track takes the nearest thumb to the point pressed,
32
+ * and moving the pointer drags it, as React Aria's and Web Awesome's sliders
33
+ * do. The thumb takes focus, and `change` is sent as the press ends.
34
+ *
31
35
  * `origin` sets where a single thumb's fill starts: at `min`, at the middle of
32
36
  * the track, or at `max`. A balance or an offset from zero fills from the
33
37
  * middle, as Zag's slider `origin` does. The fill's ends are published as
@@ -52,7 +56,7 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
52
56
  * |---|---|
53
57
  * | Right, Up | Moves the thumb up by `step`. |
54
58
  * | Left, Down | Moves the thumb down by `step`. |
55
- * | Page Up, Page Down | Moves the thumb by a larger step. |
59
+ * | Page Up, Page Down | Moves the thumb by `page-step`, or by the platform's larger step where it is not set. |
56
60
  * | Home, End | Moves the thumb to its lowest or highest value. |
57
61
  *
58
62
  * Under `readonly` none of these move the thumb, and Tab still reaches it.
@@ -97,6 +101,8 @@ declare const HelixHeadlessSlider_base: (new (...args: any[]) => import("@fusion
97
101
  * @cssproperty --slider-marker-at - Where one marker sits, as a percentage of the track.
98
102
  * @cssproperty --slider-to - Where the filled span ends, as a fraction of the track, 0 to 1: a range's upper thumb, or the farther of a single thumb and its `origin`.
99
103
  *
104
+ * @cssstate dragging - A thumb is being dragged, by its thumb or from a press on the track.
105
+ * @cssstate focus-visible - A thumb has focus that the keyboard brought, where the browser draws a focus ring.
100
106
  * @cssstate invalid - The slider has an error to show, from the browser's validation or from your `feedback`.
101
107
  */
102
108
  export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
@@ -193,6 +199,12 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
193
199
  * between its thumbs and ignores it.
194
200
  */
195
201
  origin?: 'center' | 'end' | 'start';
202
+ /**
203
+ * How far Page Up and Page Down move a thumb, in the slider's own units, as
204
+ * React Aria's `pageSize` and Zag's `largeStep` do. Absent leaves the keys
205
+ * to the platform, which moves a tenth of the track.
206
+ */
207
+ pageStep?: number;
196
208
  /**
197
209
  * Adds a second thumb, so the value is a pair.
198
210
  *
@@ -298,13 +310,52 @@ export declare class HelixHeadlessSlider extends HelixHeadlessSlider_base {
298
310
  /** Where one value sits along the track, as a fraction of it. */
299
311
  private along;
300
312
  private readonly handleInput;
313
+ /** Whether a pointer is dragging a thumb, which `:state(dragging)` says. */
314
+ private dragging;
315
+ /** Ends a drag when its pointer lifts, wherever it lifts. */
316
+ private readonly endDrag;
317
+ private startDrag;
318
+ /**
319
+ * Half a thumb, which is where the thumb's center stops short of each end
320
+ * of its input. Measured off the thumb's length as the stylesheet sets it,
321
+ * or a platform thumb's 16 pixels where none does.
322
+ */
323
+ private thumbInset;
324
+ /**
325
+ * The value under a pointer, along a thumb's input, less `inset` at each
326
+ * end. Upward on a vertical track, and from the right on a right-to-left
327
+ * page.
328
+ */
329
+ private valueAt;
330
+ /** Moves one of a range's thumbs, kept between its neighbors, and says whether it moved. */
331
+ private moveThumb;
332
+ /**
333
+ * A press on the field: on a thumb, the start of the platform's own drag,
334
+ * and on a range's track, the nearest thumb taken to the point and dragged
335
+ * from there.
336
+ *
337
+ * **A range's inputs take presses on their thumbs only**, being stacked on
338
+ * one track, so a press beside a thumb reached no input at all and did
339
+ * nothing. Measured before this: a press at 90 on a range holding 20 and 60
340
+ * left it at 20 and 60. React Aria's and Web Awesome's sliders move the
341
+ * nearest thumb there.
342
+ */
343
+ private readonly handlePress;
344
+ disconnectedCallback(): void;
301
345
  /** Relayed, because `change` does not cross a shadow boundary on its own. */
302
346
  private readonly handleChange;
303
347
  /**
304
- * Keeps a read-only thumb still under the keys that move it. Tab and every
305
- * other key pass, so focus moves on.
348
+ * Keeps a read-only thumb still under the keys that move it, and moves a
349
+ * thumb by `page-step` under Page Up and Page Down. Tab and every other key
350
+ * pass, so focus moves on.
306
351
  */
307
352
  private readonly handleKeydown;
353
+ /**
354
+ * Says whether the keyboard brought focus to a thumb, as
355
+ * `:state(focus-visible)` on the host, where a stylesheet outside can reach
356
+ * the whole control: a ring around the track, or the readout in bold.
357
+ */
358
+ private readonly handleFocusChange;
308
359
  /**
309
360
  * Reports the value: one entry, or for a named range one entry per thumb.
310
361
  * The joined pair is the state the browser restores.
@@ -4,9 +4,9 @@ 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, customElement, formatNumber, formatWords, HelixFormElement, MixinControllerValue, MixinField, reportValues, restoreValue, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, formatNumber, formatWords, HelixFormElement, isRtl, MixinControllerValue, MixinField, reportValues, restoreValue, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  const SLIDER_KEYS = new Set([
11
11
  'ArrowDown',
12
12
  'ArrowLeft',
@@ -63,6 +63,54 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
63
63
  this.requestValue(next);
64
64
  restoreValue(control, this.value);
65
65
  };
66
+ this.dragging = false;
67
+ this.endDrag = () => {
68
+ this.dragging = false;
69
+ window.removeEventListener('pointerup', this.endDrag);
70
+ window.removeEventListener('pointercancel', this.endDrag);
71
+ };
72
+ this.handlePress = (event) => {
73
+ if (this.isDisabled || this.readOnly || event.button !== 0) {
74
+ return;
75
+ }
76
+ const inputs = [...this.renderRoot.querySelectorAll('input[part="control"]')];
77
+ if (inputs.includes(event.composedPath()[0])) {
78
+ this.startDrag();
79
+ return;
80
+ }
81
+ const first = inputs[0];
82
+ const field = event.currentTarget;
83
+ if (!this.range || !first) {
84
+ return;
85
+ }
86
+ const inset = this.thumbInset(field);
87
+ const wanted = this.valueAt(event, first, inset);
88
+ let at = 0;
89
+ this.points.forEach((one, index) => {
90
+ const distance = Math.abs(one - wanted);
91
+ const best = Math.abs((this.points[at] ?? 0) - wanted);
92
+ if (distance < best || (distance === best && wanted > one)) {
93
+ at = index;
94
+ }
95
+ });
96
+ event.preventDefault();
97
+ this.moveThumb(at, wanted);
98
+ inputs[at]?.focus({ focusVisible: false, preventScroll: true });
99
+ this.startDrag();
100
+ field.setPointerCapture?.(event.pointerId);
101
+ const move = (next) => {
102
+ this.moveThumb(at, this.valueAt(next, first, inset));
103
+ };
104
+ const end = () => {
105
+ field.removeEventListener('pointermove', move);
106
+ field.removeEventListener('pointerup', end);
107
+ field.removeEventListener('pointercancel', end);
108
+ this.announceChange();
109
+ };
110
+ field.addEventListener('pointermove', move);
111
+ field.addEventListener('pointerup', end);
112
+ field.addEventListener('pointercancel', end);
113
+ };
66
114
  this.handleChange = (event) => {
67
115
  event.stopPropagation();
68
116
  if (!this.readOnly) {
@@ -72,7 +120,31 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
72
120
  this.handleKeydown = (event) => {
73
121
  if (this.readOnly && SLIDER_KEYS.has(event.key)) {
74
122
  event.preventDefault();
123
+ return;
124
+ }
125
+ const by = this.pageStep;
126
+ if ((event.key !== 'PageUp' && event.key !== 'PageDown') || !by || this.isDisabled) {
127
+ return;
75
128
  }
129
+ event.preventDefault();
130
+ const sign = event.key === 'PageUp' ? 1 : -1;
131
+ if (this.range) {
132
+ const at = Number(event.target.dataset.index ?? 0);
133
+ if (!this.moveThumb(at, this.settled((this.points[at] ?? this.min) + sign * by))) {
134
+ return;
135
+ }
136
+ }
137
+ else {
138
+ const next = String(this.settled(Number(this.value) + sign * by));
139
+ if (next === this.value || !this.requestValue(next)) {
140
+ return;
141
+ }
142
+ }
143
+ this.announceChange();
144
+ };
145
+ this.handleFocusChange = (event) => {
146
+ const input = event.target;
147
+ this.toggleState('focus-visible', event.type === 'focusin' && input.matches?.(':focus-visible') === true);
76
148
  };
77
149
  this.valueText = '';
78
150
  this.handleThumbSlot = () => {
@@ -132,6 +204,46 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
132
204
  }
133
205
  return Math.min(1, Math.max(0, (at - this.min) / span));
134
206
  }
207
+ startDrag() {
208
+ this.dragging = true;
209
+ window.addEventListener('pointerup', this.endDrag);
210
+ window.addEventListener('pointercancel', this.endDrag);
211
+ }
212
+ thumbInset(field) {
213
+ const probe = document.createElement('span');
214
+ probe.style.cssText =
215
+ 'position:absolute;visibility:hidden;inline-size:var(--_slider-thumb-length,16px)';
216
+ field.append(probe);
217
+ const length = probe.getBoundingClientRect().width;
218
+ probe.remove();
219
+ return length / 2;
220
+ }
221
+ valueAt(event, input, inset) {
222
+ const box = input.getBoundingClientRect();
223
+ const vertical = this.orientation === 'vertical';
224
+ const length = vertical ? box.height : box.width;
225
+ const from = vertical
226
+ ? box.bottom - event.clientY
227
+ : isRtl(this)
228
+ ? box.right - event.clientX
229
+ : event.clientX - box.left;
230
+ const fraction = Math.min(1, Math.max(0, (from - inset) / Math.max(1, length - inset * 2)));
231
+ return this.settled(this.min + fraction * (this.max - this.min));
232
+ }
233
+ moveThumb(at, wanted) {
234
+ const points = this.points;
235
+ const gap = this.gap;
236
+ const low = at > 0 ? (points[at - 1] ?? this.min) + gap : Number.NEGATIVE_INFINITY;
237
+ const high = at < points.length - 1 ? (points[at + 1] ?? this.max) - gap : Number.POSITIVE_INFINITY;
238
+ const next = points
239
+ .map((one, index) => (index === at ? Math.min(high, Math.max(low, wanted)) : one))
240
+ .join(',');
241
+ return next !== points.join(',') && this.requestValue(next);
242
+ }
243
+ disconnectedCallback() {
244
+ super.disconnectedCallback();
245
+ this.endDrag();
246
+ }
135
247
  sync() {
136
248
  if (this.range && this.name) {
137
249
  reportValues(this.internals, this.name, this.points.map(String));
@@ -168,6 +280,7 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
168
280
  }
169
281
  willUpdate(changed) {
170
282
  super.willUpdate(changed);
283
+ this.toggleState('dragging', this.dragging);
171
284
  if (!this.hasUpdated && this.value === '') {
172
285
  this.defaultValue = this.range
173
286
  ? `${this.min},${this.max}`
@@ -306,6 +419,9 @@ let HelixHeadlessSlider = class HelixHeadlessSlider extends MixinControllerValue
306
419
  : nothing}
307
420
  <div
308
421
  part="field"
422
+ @focusin=${this.handleFocusChange}
423
+ @focusout=${this.handleFocusChange}
424
+ @pointerdown=${this.handlePress}
309
425
  data-markers=${this.markerValues.length > 0 ? '' : nothing}
310
426
  data-range=${this.range ? '' : nothing}
311
427
  data-thumbs=${this.thumbContent ? '' : nothing}
@@ -405,6 +521,9 @@ __decorate([
405
521
  __decorate([
406
522
  property({ reflect: true })
407
523
  ], HelixHeadlessSlider.prototype, "origin", void 0);
524
+ __decorate([
525
+ property({ attribute: 'page-step', type: Number })
526
+ ], HelixHeadlessSlider.prototype, "pageStep", void 0);
408
527
  __decorate([
409
528
  property({ reflect: true, type: Boolean })
410
529
  ], HelixHeadlessSlider.prototype, "range", void 0);
@@ -428,6 +547,9 @@ __decorate([
428
547
  __decorate([
429
548
  property({ type: Boolean })
430
549
  ], HelixHeadlessSlider.prototype, "tooltip", void 0);
550
+ __decorate([
551
+ state()
552
+ ], HelixHeadlessSlider.prototype, "dragging", void 0);
431
553
  __decorate([
432
554
  property({ attribute: 'value-text' })
433
555
  ], HelixHeadlessSlider.prototype, "valueText", void 0);