@fusion.dev/helix-headless 0.144.0 → 0.145.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 (155) hide show
  1. package/custom-elements.json +8312 -881
  2. package/dist/accordion/item/accordion-item.d.ts +48 -12
  3. package/dist/accordion/item/accordion-item.js +26 -5
  4. package/dist/accordion/root/accordion-root.d.ts +6 -0
  5. package/dist/accordion/root/accordion-root.js +7 -1
  6. package/dist/announcement/root/announcement-root.d.ts +21 -1
  7. package/dist/announcement/root/announcement-root.js +36 -1
  8. package/dist/app-header/root/app-header-root.d.ts +7 -0
  9. package/dist/app-header/root/app-header-root.js +5 -1
  10. package/dist/app-header/sectioning.d.ts +7 -0
  11. package/dist/app-header/sectioning.js +26 -0
  12. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +32 -1
  13. package/dist/breadcrumbs/root/breadcrumbs-root.js +25 -2
  14. package/dist/canvas/root/canvas-root.d.ts +38 -3
  15. package/dist/canvas/root/canvas-root.js +121 -40
  16. package/dist/carousel/root/carousel-root.d.ts +90 -0
  17. package/dist/carousel/root/carousel-root.js +134 -12
  18. package/dist/chat/composer/chat-composer.d.ts +38 -6
  19. package/dist/chat/composer/chat-composer.js +42 -9
  20. package/dist/chat/message/chat-message.d.ts +32 -1
  21. package/dist/chat/message/chat-message.js +25 -9
  22. package/dist/chat/naming.d.ts +22 -0
  23. package/dist/chat/naming.js +12 -0
  24. package/dist/chat/relative-clock.d.ts +39 -0
  25. package/dist/chat/relative-clock.js +31 -0
  26. package/dist/chat/root/chat-root.d.ts +36 -0
  27. package/dist/chat/root/chat-root.js +24 -1
  28. package/dist/code-block/root/code-block-root.d.ts +8 -1
  29. package/dist/code-block/root/code-block-root.js +5 -5
  30. package/dist/copy-button/root/copy-button-root.d.ts +21 -1
  31. package/dist/copy-button/root/copy-button-root.js +2 -2
  32. package/dist/data-table/root/data-table-root.d.ts +150 -2
  33. package/dist/data-table/root/data-table-root.js +303 -29
  34. package/dist/data-table/tree.d.ts +40 -0
  35. package/dist/data-table/tree.js +55 -0
  36. package/dist/description-list/item/description-list-item.d.ts +17 -2
  37. package/dist/description-list/item/description-list-item.js +15 -2
  38. package/dist/description-list/root/description-list-root.d.ts +27 -1
  39. package/dist/description-list/root/description-list-root.js +26 -1
  40. package/dist/drilldown/item/drilldown-item.d.ts +11 -6
  41. package/dist/drilldown/item/drilldown-item.js +8 -5
  42. package/dist/drilldown/root/drilldown-root.d.ts +11 -0
  43. package/dist/drilldown/root/drilldown-root.js +8 -3
  44. package/dist/element-events.generated.js +53 -8
  45. package/dist/filter-input/menu/filter-menu.d.ts +3 -0
  46. package/dist/filter-input/menu/filter-menu.js +8 -4
  47. package/dist/image/root/image-root.js +9 -1
  48. package/dist/input/root/input-root.d.ts +1 -1
  49. package/dist/input/root/input-root.js +2 -2
  50. package/dist/list/item/list-item.d.ts +68 -1
  51. package/dist/list/item/list-item.js +66 -4
  52. package/dist/list/numbering.d.ts +14 -0
  53. package/dist/list/numbering.js +8 -0
  54. package/dist/list/root/list-root.d.ts +106 -9
  55. package/dist/list/root/list-root.js +189 -4
  56. package/dist/list/selection.d.ts +19 -0
  57. package/dist/list/selection.js +21 -0
  58. package/dist/listbox/root/listbox-root.js +1 -2
  59. package/dist/map/root/map-root.d.ts +6 -2
  60. package/dist/map/root/map-root.js +5 -2
  61. package/dist/megamenu/root/megamenu-root.d.ts +21 -0
  62. package/dist/megamenu/root/megamenu-root.js +16 -2
  63. package/dist/menu/index.d.ts +1 -0
  64. package/dist/menu/index.js +1 -0
  65. package/dist/menu/item/menu-item.d.ts +38 -11
  66. package/dist/menu/item/menu-item.js +33 -1
  67. package/dist/menu/root/menu-root.d.ts +25 -1
  68. package/dist/menu/root/menu-root.js +63 -6
  69. package/dist/menu/section/menu-section.d.ts +55 -0
  70. package/dist/menu/section/menu-section.js +60 -0
  71. package/dist/navigation/root/navigation-root.d.ts +16 -1
  72. package/dist/navigation/root/navigation-root.js +27 -1
  73. package/dist/notifications/root/notifications-root.d.ts +18 -0
  74. package/dist/notifications/root/notifications-root.js +17 -1
  75. package/dist/object-inspector/root/object-inspector-root.d.ts +97 -16
  76. package/dist/object-inspector/root/object-inspector-root.js +179 -79
  77. package/dist/object-inspector/rows.d.ts +57 -0
  78. package/dist/object-inspector/rows.js +86 -0
  79. package/dist/outline/levels.d.ts +31 -0
  80. package/dist/outline/levels.js +23 -0
  81. package/dist/outline/root/outline-root.d.ts +68 -3
  82. package/dist/outline/root/outline-root.js +73 -8
  83. package/dist/pagination/size/pagination-size.d.ts +11 -2
  84. package/dist/pagination/size/pagination-size.js +19 -4
  85. package/dist/radio/group/radio-group.d.ts +4 -10
  86. package/dist/radio/group/radio-group.js +5 -10
  87. package/dist/rating/root/rating-root.d.ts +1 -1
  88. package/dist/rating/root/rating-root.js +2 -2
  89. package/dist/scroll-area/root/scroll-area-root.d.ts +113 -0
  90. package/dist/scroll-area/root/scroll-area-root.js +218 -3
  91. package/dist/scroll-area/scrollbar.d.ts +65 -0
  92. package/dist/scroll-area/scrollbar.js +24 -0
  93. package/dist/select/root/select-root.js +1 -1
  94. package/dist/splitter/root/splitter-root.d.ts +13 -0
  95. package/dist/splitter/root/splitter-root.js +20 -2
  96. package/dist/splitter/snap.d.ts +9 -0
  97. package/dist/splitter/snap.js +12 -0
  98. package/dist/steps/reach.d.ts +15 -0
  99. package/dist/steps/reach.js +12 -0
  100. package/dist/steps/root/steps-root.d.ts +30 -0
  101. package/dist/steps/root/steps-root.js +53 -0
  102. package/dist/steps/step/step.d.ts +38 -2
  103. package/dist/steps/step/step.js +28 -6
  104. package/dist/table/column/table-column.d.ts +11 -0
  105. package/dist/table/column/table-column.js +6 -1
  106. package/dist/table/root/table-root.d.ts +101 -0
  107. package/dist/table/root/table-root.js +166 -5
  108. package/dist/table/row/table-row.d.ts +47 -1
  109. package/dist/table/row/table-row.js +39 -2
  110. package/dist/tabs/panel/tab-panel.d.ts +9 -5
  111. package/dist/tabs/panel/tab-panel.js +3 -4
  112. package/dist/tabs/root/tabs-root.d.ts +25 -1
  113. package/dist/tabs/root/tabs-root.js +21 -3
  114. package/dist/tabs/tab/tab.d.ts +36 -4
  115. package/dist/tabs/tab/tab.js +46 -3
  116. package/dist/timeline/group/timeline-group.d.ts +46 -0
  117. package/dist/timeline/group/timeline-group.js +37 -0
  118. package/dist/timeline/index.d.ts +1 -0
  119. package/dist/timeline/index.js +1 -0
  120. package/dist/timeline/root/timeline-root.d.ts +10 -3
  121. package/dist/toast/item/toast-item.d.ts +10 -17
  122. package/dist/toast/item/toast-item.js +15 -36
  123. package/dist/toolbar/root/toolbar-root.d.ts +13 -0
  124. package/dist/toolbar/root/toolbar-root.js +15 -5
  125. package/dist/tree/item/tree-item.d.ts +54 -1
  126. package/dist/tree/item/tree-item.js +89 -20
  127. package/dist/tree/root/tree-root.d.ts +37 -0
  128. package/dist/tree/root/tree-root.js +51 -6
  129. package/dist/tree/selection.d.ts +24 -0
  130. package/dist/tree/selection.js +26 -0
  131. package/dist/video/media-events.d.ts +49 -0
  132. package/dist/video/media-events.js +39 -0
  133. package/dist/video/root/video-root.d.ts +99 -13
  134. package/dist/video/root/video-root.js +84 -47
  135. package/dist/video-playlist/menu/video-playlist-menu.d.ts +3 -0
  136. package/dist/video-playlist/menu/video-playlist-menu.js +2 -1
  137. package/dist/video-playlist/order.d.ts +41 -0
  138. package/dist/video-playlist/order.js +34 -0
  139. package/dist/video-playlist/root/video-playlist-root.d.ts +70 -2
  140. package/dist/video-playlist/root/video-playlist-root.js +60 -16
  141. package/dist/virtual-list/controls.d.ts +4 -0
  142. package/dist/virtual-list/controls.js +30 -0
  143. package/dist/virtual-list/fill.d.ts +30 -0
  144. package/dist/virtual-list/fill.js +39 -0
  145. package/dist/virtual-list/keys.d.ts +15 -0
  146. package/dist/virtual-list/keys.js +24 -0
  147. package/dist/virtual-list/root/virtual-list-root.d.ts +155 -23
  148. package/dist/virtual-list/root/virtual-list-root.js +175 -34
  149. package/lint.json +1 -1
  150. package/package.json +5 -5
  151. package/types/jsx.d.ts +3778 -801
  152. package/types/svelte.d.ts +2 -0
  153. package/types/vue.d.ts +782 -215
  154. package/dist/listbox/range.d.ts +0 -17
  155. package/dist/listbox/range.js +0 -5
@@ -4,14 +4,20 @@ 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, HelixElement } from '@fusion.dev/helix-core';
7
+ import { capturePointer, customElement, DelayController, HelixElement, isRtl, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
+ import { hasScrollbar, offsetAt, scrollFraction, scrollPerPixel, showsScrollbar, thumbFraction, } from '../scrollbar.js';
10
11
  const EDGES = ['block-start', 'block-end', 'inline-start', 'inline-end'];
11
12
  let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement {
12
13
  constructor() {
13
14
  super(...arguments);
15
+ this.hideDelay = 600;
14
16
  this.label = '';
17
+ this.active = false;
18
+ this.hovered = false;
19
+ this.over = false;
20
+ this.hide = new DelayController(this, { renders: false });
15
21
  this.more = [];
16
22
  this.scrolls = false;
17
23
  this.measure = () => {
@@ -36,6 +42,56 @@ let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement
36
42
  }
37
43
  this.more = found;
38
44
  this.scrolls = box.scrollHeight > box.clientHeight || box.scrollWidth > box.clientWidth;
45
+ if (this.scrollbars) {
46
+ this.readBars(box);
47
+ }
48
+ };
49
+ this.handleVerticalDown = (event) => {
50
+ this.grab(event, 'vertical');
51
+ };
52
+ this.handleHorizontalDown = (event) => {
53
+ this.grab(event, 'horizontal');
54
+ };
55
+ this.handleDragMove = (event) => {
56
+ const drag = this.drag;
57
+ const box = this.scroller;
58
+ if (!drag || !box) {
59
+ return;
60
+ }
61
+ const pointer = drag.axis === 'vertical' ? event.clientY : event.clientX;
62
+ this.scrollAlong(box, drag.axis, drag.from + (pointer - drag.pointer) * drag.sign * drag.perPixel);
63
+ };
64
+ this.handleDragEnd = () => {
65
+ if (!this.drag) {
66
+ return;
67
+ }
68
+ this.drag = undefined;
69
+ this.wake();
70
+ this.requestUpdate();
71
+ };
72
+ this.handleBarEnter = () => {
73
+ this.over = true;
74
+ };
75
+ this.handleBarLeave = () => {
76
+ this.over = false;
77
+ this.wake();
78
+ };
79
+ this.handleEnter = () => {
80
+ this.hovered = true;
81
+ };
82
+ this.handleLeave = () => {
83
+ this.hovered = false;
84
+ if (this.scrollbars === 'hover') {
85
+ this.wake();
86
+ }
87
+ };
88
+ this.handleScroll = (event) => {
89
+ this.measure();
90
+ this.wake();
91
+ this.relayNativeEvent(event);
92
+ };
93
+ this.handleScrollEnd = (event) => {
94
+ this.relayNativeEvent(event);
39
95
  };
40
96
  this.handleSlotChange = (event) => {
41
97
  const slot = event.target;
@@ -48,8 +104,84 @@ let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement
48
104
  get scroller() {
49
105
  return this.partNamed('viewport');
50
106
  }
107
+ extent(box, axis) {
108
+ return axis === 'vertical'
109
+ ? { client: box.clientHeight, offset: box.scrollTop, scroll: box.scrollHeight }
110
+ : { client: box.clientWidth, offset: Math.abs(box.scrollLeft), scroll: box.scrollWidth };
111
+ }
112
+ readBars(box) {
113
+ const mode = this.scrollbars ?? 'auto';
114
+ const style = getComputedStyle(box);
115
+ const bar = (axis, overflow) => {
116
+ const extent = this.extent(box, axis);
117
+ return {
118
+ along: scrollFraction(extent),
119
+ drawn: hasScrollbar(mode, !['clip', 'hidden', 'visible'].includes(overflow), extent.offset > 1 || extent.client < extent.scroll - 1),
120
+ thumb: thumbFraction(extent),
121
+ };
122
+ };
123
+ const next = {
124
+ horizontal: bar('horizontal', style.overflowX),
125
+ vertical: bar('vertical', style.overflowY),
126
+ };
127
+ if (JSON.stringify(next) !== JSON.stringify(this.bars)) {
128
+ this.bars = next;
129
+ }
130
+ }
131
+ wake() {
132
+ if (this.scrollbars !== 'scroll' && this.scrollbars !== 'hover') {
133
+ return;
134
+ }
135
+ this.active = true;
136
+ this.hide.start(this.hideDelay, () => {
137
+ this.active = false;
138
+ });
139
+ }
140
+ scrollAlong(box, axis, offset) {
141
+ box.scrollTo(axis === 'vertical'
142
+ ? { behavior: 'instant', top: offset }
143
+ : { behavior: 'instant', left: isRtl(box) ? -offset : offset });
144
+ }
145
+ grab(event, axis) {
146
+ const box = this.scroller;
147
+ const bar = event.currentTarget;
148
+ const thumb = bar.querySelector('[part~="thumb"]');
149
+ if (event.button !== 0 || !box || !thumb) {
150
+ return;
151
+ }
152
+ event.preventDefault();
153
+ const vertical = axis === 'vertical';
154
+ const rtl = !vertical && isRtl(box);
155
+ const track = bar.getBoundingClientRect();
156
+ const held = thumb.getBoundingClientRect();
157
+ const length = vertical ? track.height : track.width;
158
+ const size = vertical ? held.height : held.width;
159
+ const extent = this.extent(box, axis);
160
+ const pointer = vertical ? event.clientY : event.clientX;
161
+ let from = extent.offset;
162
+ if (!thumb.contains(event.target)) {
163
+ const along = vertical
164
+ ? pointer - track.top
165
+ : rtl
166
+ ? track.right - pointer
167
+ : pointer - track.left;
168
+ from = offsetAt(extent, along, length, size);
169
+ this.scrollAlong(box, axis, from);
170
+ }
171
+ this.drag = {
172
+ axis,
173
+ from,
174
+ perPixel: scrollPerPixel(extent, length, size),
175
+ pointer,
176
+ sign: rtl ? -1 : 1,
177
+ };
178
+ capturePointer(bar, event.pointerId);
179
+ this.requestUpdate();
180
+ }
51
181
  connectedCallback() {
52
182
  super.connectedCallback();
183
+ this.addEventListener('pointerenter', this.handleEnter);
184
+ this.addEventListener('pointerleave', this.handleLeave);
53
185
  this.watcher = new ResizeObserver(this.measure);
54
186
  if (this.hasUpdated) {
55
187
  this.watchContent();
@@ -57,8 +189,14 @@ let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement
57
189
  }
58
190
  disconnectedCallback() {
59
191
  super.disconnectedCallback();
192
+ this.removeEventListener('pointerenter', this.handleEnter);
193
+ this.removeEventListener('pointerleave', this.handleLeave);
60
194
  this.watcher?.disconnect();
61
195
  this.watcher = undefined;
196
+ this.drag = undefined;
197
+ this.active = false;
198
+ this.hovered = false;
199
+ this.over = false;
62
200
  }
63
201
  firstUpdated(changed) {
64
202
  super.firstUpdated?.(changed);
@@ -79,11 +217,20 @@ let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement
79
217
  }
80
218
  updated(changed) {
81
219
  super.updated?.(changed);
220
+ if (changed.has('scrollbars') && this.hasUpdated && this.scrollbars) {
221
+ const box = this.scroller;
222
+ if (box) {
223
+ this.readBars(box);
224
+ }
225
+ }
82
226
  this.toggleState('scrolls', this.scrolls);
83
227
  this.toggleState('more-block-start', this.more.includes('block-start'));
84
228
  this.toggleState('more-block-end', this.more.includes('block-end'));
85
229
  this.toggleState('more-inline-start', this.more.includes('inline-start'));
86
230
  this.toggleState('more-inline-end', this.more.includes('inline-end'));
231
+ const drawn = this.scrollbars ? this.bars : undefined;
232
+ this.toggleState('scrollbar-horizontal', drawn?.horizontal.drawn === true);
233
+ this.toggleState('scrollbar-vertical', drawn?.vertical.drawn === true);
87
234
  }
88
235
  render() {
89
236
  return html `<div
@@ -92,7 +239,8 @@ let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement
92
239
  role=${this.label ? 'region' : nothing}
93
240
  aria-label=${this.label || nothing}
94
241
  tabindex=${this.scrolls ? '0' : nothing}
95
- @scroll=${this.measure}
242
+ @scroll=${this.handleScroll}
243
+ @scrollend=${this.handleScrollEnd}
96
244
  >
97
245
  <slot @slotchange=${this.handleSlotChange}></slot>
98
246
  </div>
@@ -104,12 +252,79 @@ let HelixHeadlessScrollArea = class HelixHeadlessScrollArea extends HelixElement
104
252
  <div
105
253
  part="edges"
106
254
  aria-hidden="true"
107
- ></div>`;
255
+ ></div>${this.scrollbars ? this.renderScrollbars(this.scrollbars) : nothing}`;
256
+ }
257
+ renderScrollbars(mode) {
258
+ const bars = this.bars;
259
+ const moment = {
260
+ active: this.active,
261
+ held: this.over || this.drag !== undefined,
262
+ hovered: this.hovered,
263
+ };
264
+ const shown = showsScrollbar(mode, moment);
265
+ const facts = (bar) => bar
266
+ ? `--scroll-area-thumb-fraction: ${bar.thumb}; --scroll-area-scroll-fraction: ${bar.along}`
267
+ : nothing;
268
+ const vertical = bars?.vertical.drawn === true;
269
+ const horizontal = bars?.horizontal.drawn === true;
270
+ return html `<div
271
+ part="scrollbar scrollbar-vertical${vertical && shown ? ' scrollbar-visible' : ''}"
272
+ aria-hidden="true"
273
+ ?hidden=${!vertical}
274
+ style=${facts(bars?.vertical)}
275
+ @pointerdown=${this.handleVerticalDown}
276
+ @pointermove=${this.handleDragMove}
277
+ @pointerup=${this.handleDragEnd}
278
+ @pointercancel=${this.handleDragEnd}
279
+ @lostpointercapture=${this.handleDragEnd}
280
+ @pointerenter=${this.handleBarEnter}
281
+ @pointerleave=${this.handleBarLeave}
282
+ >
283
+ <div part="thumb thumb-vertical"></div>
284
+ </div>
285
+ <div
286
+ part="scrollbar scrollbar-horizontal${horizontal && shown ? ' scrollbar-visible' : ''}"
287
+ aria-hidden="true"
288
+ ?hidden=${!horizontal}
289
+ style=${facts(bars?.horizontal)}
290
+ @pointerdown=${this.handleHorizontalDown}
291
+ @pointermove=${this.handleDragMove}
292
+ @pointerup=${this.handleDragEnd}
293
+ @pointercancel=${this.handleDragEnd}
294
+ @lostpointercapture=${this.handleDragEnd}
295
+ @pointerenter=${this.handleBarEnter}
296
+ @pointerleave=${this.handleBarLeave}
297
+ >
298
+ <div part="thumb thumb-horizontal"></div>
299
+ </div>
300
+ <div
301
+ part="corner"
302
+ aria-hidden="true"
303
+ ?hidden=${!(vertical && horizontal)}
304
+ ></div>`;
108
305
  }
109
306
  };
307
+ __decorate([
308
+ property({ attribute: 'hide-delay', type: Number })
309
+ ], HelixHeadlessScrollArea.prototype, "hideDelay", void 0);
110
310
  __decorate([
111
311
  property()
112
312
  ], HelixHeadlessScrollArea.prototype, "label", void 0);
313
+ __decorate([
314
+ property({ reflect: true })
315
+ ], HelixHeadlessScrollArea.prototype, "scrollbars", void 0);
316
+ __decorate([
317
+ state()
318
+ ], HelixHeadlessScrollArea.prototype, "bars", void 0);
319
+ __decorate([
320
+ state()
321
+ ], HelixHeadlessScrollArea.prototype, "active", void 0);
322
+ __decorate([
323
+ state()
324
+ ], HelixHeadlessScrollArea.prototype, "hovered", void 0);
325
+ __decorate([
326
+ state()
327
+ ], HelixHeadlessScrollArea.prototype, "over", void 0);
113
328
  __decorate([
114
329
  state()
115
330
  ], HelixHeadlessScrollArea.prototype, "more", void 0);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The arithmetic behind the scrollbars a scroll area draws under `scrollbars`.
3
+ *
4
+ * Every length here is along one axis and counted from that axis' start, which
5
+ * is the right-hand edge on a right-to-left page for the horizontal one. The
6
+ * component turns a pointer and a `scrollLeft` into those counts, so nothing
7
+ * below has to know which way the line runs.
8
+ */
9
+ /** Which scrollbars are drawn, and when. Absent keeps the browser's own. */
10
+ export type Scrollbars = 'always' | 'auto' | 'hover' | 'scroll';
11
+ /** One axis of the viewport, read off the box. */
12
+ export interface Extent {
13
+ /** How much of the content the viewport shows, `clientHeight` or `clientWidth`. */
14
+ client: number;
15
+ /** How far from its start the axis is scrolled, never negative. */
16
+ offset: number;
17
+ /** The whole length of the content, `scrollHeight` or `scrollWidth`. */
18
+ scroll: number;
19
+ }
20
+ /**
21
+ * The share of the track the thumb covers, from 0 to 1: how much of the content
22
+ * is in view. A box with no content, or one that shows all of it, is 1.
23
+ */
24
+ export declare const thumbFraction: ({ client, scroll }: Extent) => number;
25
+ /**
26
+ * How far along the axis is scrolled, from 0 at the start to 1 at the end. An
27
+ * axis with nothing to scroll is 0.
28
+ */
29
+ export declare const scrollFraction: ({ client, offset, scroll }: Extent) => number;
30
+ /**
31
+ * How far the content moves for each pixel the thumb is dragged.
32
+ *
33
+ * The thumb travels the track less its own length, and the content travels
34
+ * its length less the view, so the ratio of the two is the gearing. Zero where
35
+ * the thumb fills the track and cannot move.
36
+ */
37
+ export declare const scrollPerPixel: (extent: Extent, track: number, thumb: number) => number;
38
+ /**
39
+ * The offset that puts the middle of the thumb under a press on the track.
40
+ *
41
+ * `along` is where the press landed, counted from the track's start. The thumb
42
+ * stops at either end of the track, so a press near an end scrolls to that end.
43
+ */
44
+ export declare const offsetAt: (extent: Extent, along: number, track: number, thumb: number) => number;
45
+ /**
46
+ * Whether an axis has a scrollbar at all: it can scroll, and under every value
47
+ * but `always` it has something to scroll.
48
+ *
49
+ * An axis whose `overflow` is `hidden` or `clip` cannot be scrolled by a reader,
50
+ * so it gets no scrollbar whatever `scrollbars` says. That is how a consumer
51
+ * keeps a horizontal bar off a list that only scrolls down.
52
+ */
53
+ export declare const hasScrollbar: (mode: Scrollbars, scrollable: boolean, overflows: boolean) => boolean;
54
+ /**
55
+ * Whether a scrollbar that exists is showing at this moment.
56
+ *
57
+ * `auto` and `always` show it the whole time. `scroll` shows it while the
58
+ * content is moving and for the hide delay after, and while the pointer holds
59
+ * it. `hover` adds the time the pointer is over the scroll area.
60
+ */
61
+ export declare const showsScrollbar: (mode: Scrollbars, moment: {
62
+ active: boolean;
63
+ held: boolean;
64
+ hovered: boolean;
65
+ }) => boolean;
@@ -0,0 +1,24 @@
1
+ const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
2
+ export const thumbFraction = ({ client, scroll }) => scroll <= 0 ? 1 : clamp(client / scroll, 0, 1);
3
+ export const scrollFraction = ({ client, offset, scroll }) => {
4
+ const room = scroll - client;
5
+ return room <= 0 ? 0 : clamp(offset / room, 0, 1);
6
+ };
7
+ export const scrollPerPixel = (extent, track, thumb) => {
8
+ const run = track - thumb;
9
+ return run <= 0 ? 0 : Math.max(0, extent.scroll - extent.client) / run;
10
+ };
11
+ export const offsetAt = (extent, along, track, thumb) => {
12
+ const run = track - thumb;
13
+ if (run <= 0) {
14
+ return 0;
15
+ }
16
+ return clamp((along - thumb / 2) / run, 0, 1) * Math.max(0, extent.scroll - extent.client);
17
+ };
18
+ export const hasScrollbar = (mode, scrollable, overflows) => scrollable && (mode === 'always' || overflows);
19
+ export const showsScrollbar = (mode, moment) => {
20
+ if (mode === 'auto' || mode === 'always') {
21
+ return true;
22
+ }
23
+ return moment.active || moment.held || (mode === 'hover' && moment.hovered);
24
+ };
@@ -222,7 +222,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
222
222
  ?multiple=${this.multiple}
223
223
  ?filterable=${this.filterable}
224
224
  manual
225
- aria-label=${this.label || nothing}
225
+ aria-label=${this.label || this.getAttribute('aria-label') || nothing}
226
226
  @keydown=${this.handleListKeydown}
227
227
  @helix-before-change=${this.handleChange}
228
228
  @change=${this.stopInner}
@@ -179,6 +179,17 @@ export declare class HelixHeadlessSplitter extends HelixElement {
179
179
  */
180
180
  get position(): number;
181
181
  set position(next: number);
182
+ /**
183
+ * Where a dragged divider snaps, comma separated, as Web Awesome's split
184
+ * panel does: a bare number is a percentage and a length such as `240px` is
185
+ * measured along the splitter, so `snap="25, 50, 75"` or `snap="240px, 50"`.
186
+ * The arrow keys move by `step` and do not snap.
187
+ */
188
+ snap: string;
189
+ /**
190
+ * How close a drag has to come to a snap point to land on it, in pixels.
191
+ */
192
+ snapThreshold: number;
182
193
  /**
183
194
  * How far one arrow key press moves the divider, as a percentage.
184
195
  */
@@ -434,6 +445,8 @@ export declare class HelixHeadlessSplitter extends HelixElement {
434
445
  private settle;
435
446
  private readonly handlePointerDown;
436
447
  private readonly handlePointerMove;
448
+ /** Where a drag at `position` lands once `snap` has had its say. */
449
+ private snapped;
437
450
  private readonly handlePointerUp;
438
451
  /**
439
452
  * The keys a separator answers.
@@ -5,9 +5,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  var HelixHeadlessSplitter_1;
8
- import { AriaForwardController, capturePointer, closestInstance, customElement, HelixBeforeCollapseEvent, HelixBeforeResizeEvent, HelixCollapseEvent, HelixElement, HelixResizedEvent, inlineStep, isRtl, requestChange, } from '@fusion.dev/helix-core';
8
+ import { AriaForwardController, capturePointer, closestInstance, commaList, customElement, HelixBeforeCollapseEvent, HelixBeforeResizeEvent, HelixCollapseEvent, HelixElement, HelixResizedEvent, inlineStep, isRtl, requestChange, } from '@fusion.dev/helix-core';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
+ import { snapTo } from '../snap.js';
11
12
  const STACK_BELOW = 512;
12
13
  let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitter extends HelixElement {
13
14
  constructor() {
@@ -23,6 +24,8 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
23
24
  });
24
25
  this.max = 100;
25
26
  this.min = 0;
27
+ this.snap = '';
28
+ this.snapThreshold = 12;
26
29
  this.step = 2;
27
30
  this.positionState = undefined;
28
31
  this.collapsedState = undefined;
@@ -57,7 +60,7 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
57
60
  const along = this.vertical ? event.clientY : event.clientX;
58
61
  const raw = ((along - origin.start) / origin.length) * 100;
59
62
  const rtl = !this.vertical && isRtl(this);
60
- this.move(rtl ? 100 - raw : raw);
63
+ this.move(this.snapped(rtl ? 100 - raw : raw, origin.length));
61
64
  };
62
65
  this.handlePointerUp = (event) => {
63
66
  if (!this.origin) {
@@ -345,6 +348,15 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
345
348
  });
346
349
  }
347
350
  }
351
+ snapped(position, length) {
352
+ if (this.snap === '' || length <= 0) {
353
+ return position;
354
+ }
355
+ const points = commaList(this.snap)
356
+ .map((point) => this.toPercent(point, Number.NaN))
357
+ .filter((point) => Number.isFinite(point));
358
+ return snapTo(position, points, (this.snapThreshold / length) * 100);
359
+ }
348
360
  travel(key) {
349
361
  if (!this.vertical) {
350
362
  return inlineStep(this, key);
@@ -541,6 +553,12 @@ __decorate([
541
553
  __decorate([
542
554
  property({ type: Number })
543
555
  ], HelixHeadlessSplitter.prototype, "position", null);
556
+ __decorate([
557
+ property()
558
+ ], HelixHeadlessSplitter.prototype, "snap", void 0);
559
+ __decorate([
560
+ property({ attribute: 'snap-threshold', type: Number })
561
+ ], HelixHeadlessSplitter.prototype, "snapThreshold", void 0);
544
562
  __decorate([
545
563
  property({ type: Number })
546
564
  ], HelixHeadlessSplitter.prototype, "step", void 0);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Where a dragged divider lands: on the nearest snap point within `threshold`,
3
+ * or where the pointer is.
4
+ *
5
+ * Every number is a percentage of the splitter's box, so the caller turns a
6
+ * point written as a length, and the threshold, into one first. Ties go to the
7
+ * point written first.
8
+ */
9
+ export declare const snapTo: (position: number, points: readonly number[], threshold: number) => number;
@@ -0,0 +1,12 @@
1
+ export const snapTo = (position, points, threshold) => {
2
+ let best;
3
+ let distance = Number.POSITIVE_INFINITY;
4
+ for (const point of points) {
5
+ const away = Math.abs(point - position);
6
+ if (away <= threshold && away < distance) {
7
+ best = point;
8
+ distance = away;
9
+ }
10
+ }
11
+ return best ?? position;
12
+ };
@@ -0,0 +1,15 @@
1
+ /** What linear mode reads of a step. */
2
+ export interface ReachableStep {
3
+ /** Whether the stage is finished. */
4
+ complete: boolean;
5
+ /** Whether a reader may pass the stage without finishing it. */
6
+ optional: boolean;
7
+ }
8
+ /**
9
+ * Whether a reader on step `from` may go to step `to`, in a linear sequence.
10
+ *
11
+ * Back is always allowed, as Ark UI's linear steps allow it. Forward is allowed
12
+ * only where every stage from the one the reader is on up to the target is
13
+ * complete or optional, so no stage is passed unfinished.
14
+ */
15
+ export declare const canReach: (steps: readonly ReachableStep[], from: number, to: number) => boolean;
@@ -0,0 +1,12 @@
1
+ export const canReach = (steps, from, to) => {
2
+ if (to <= from) {
3
+ return true;
4
+ }
5
+ for (let at = Math.max(from, 0); at < to; at += 1) {
6
+ const step = steps[at];
7
+ if (step && !step.complete && !step.optional) {
8
+ return false;
9
+ }
10
+ }
11
+ return true;
12
+ };
@@ -13,6 +13,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
13
13
  * sequence is correct in the server's markup, where the root cannot count its
14
14
  * slotted children.
15
15
  *
16
+ * `linear` keeps a reader from passing a stage they have not finished, as Ark
17
+ * UI's linear steps do: a stage ahead is out of reach, and its button disabled,
18
+ * until every stage before it is `complete` or `optional`. Going back is always
19
+ * allowed. To check a stage before a reader leaves it, cancel
20
+ * `helix-before-activate` while it is not valid. `next()` and `previous()` move
21
+ * one stage either way, under the same rules, for your own buttons.
22
+ *
16
23
  * @since 0.1.0
17
24
  * @status stable
18
25
  * @category navigation
@@ -33,6 +40,11 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
33
40
  * needs a name of its own rather than borrowing the heading above it.
34
41
  */
35
42
  label: string;
43
+ /**
44
+ * Whether a reader has to finish each stage, or pass an `optional` one,
45
+ * before going on to the next.
46
+ */
47
+ linear: boolean;
36
48
  /**
37
49
  * Which way the steps run.
38
50
  *
@@ -62,6 +74,24 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
62
74
  variant?: 'numeric';
63
75
  /** The steps, in the order the consumer wrote them. */
64
76
  private get steps();
77
+ /** The index of the stage the reader is on, or `-1`. */
78
+ private get at();
79
+ /**
80
+ * Moves to the next stage that can be gone to, as Ark UI's `goToNextStep`
81
+ * does. Returns whether it moved: not past the last stage, not into a stage
82
+ * `linear` holds out of reach, and not where a listener refused.
83
+ */
84
+ next(): boolean;
85
+ /** Moves to the stage before, as Ark UI's `goToPrevStep` does. Returns whether it moved. */
86
+ previous(): boolean;
87
+ /** Moves `delta` stages, skipping disabled ones. */
88
+ private step;
89
+ /** Whether the stage at `index` can be gone to from where the reader is. */
90
+ private reachable;
91
+ /** Marks the stages a linear sequence holds out of reach. Client only. */
92
+ private lock;
93
+ /** Watches what decides a stage's reach: which is current, which are done. */
94
+ private watcher;
65
95
  /**
66
96
  * Puts `active` on whichever step owns it.
67
97
  *
@@ -7,23 +7,62 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria, MixinControllerActive, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { canReach } from '../reach.js';
10
11
  let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(HelixElement) {
11
12
  constructor() {
12
13
  super(...arguments);
13
14
  this.label = 'Steps';
15
+ this.linear = false;
14
16
  this.handleSelection = (event) => {
15
17
  if (event.defaultPrevented || !this.steps.includes(event.target)) {
16
18
  return;
17
19
  }
20
+ if (!this.reachable(this.steps.indexOf(event.target))) {
21
+ return;
22
+ }
18
23
  this.requestActive(event.detail.value);
19
24
  };
20
25
  this.handleSlotChange = () => {
21
26
  this.sync();
27
+ this.lock();
22
28
  };
23
29
  }
24
30
  get steps() {
25
31
  return [...this.querySelectorAll(':scope > *')].filter((one) => 'current' in one);
26
32
  }
33
+ get at() {
34
+ return this.steps.findIndex((step) => step.current);
35
+ }
36
+ next() {
37
+ return this.step(1);
38
+ }
39
+ previous() {
40
+ return this.step(-1);
41
+ }
42
+ step(delta) {
43
+ const steps = this.steps;
44
+ let index = this.at + delta;
45
+ while (steps[index]?.disabled) {
46
+ index += delta;
47
+ }
48
+ const target = steps[index];
49
+ if (!target || !this.reachable(index)) {
50
+ return false;
51
+ }
52
+ return this.requestActive(target.value);
53
+ }
54
+ reachable(index) {
55
+ return !this.linear || canReach(this.steps, this.at, index);
56
+ }
57
+ lock() {
58
+ const steps = this.steps;
59
+ steps.forEach((step, index) => {
60
+ const locked = !this.reachable(index);
61
+ whenHydrated(step, () => {
62
+ step.locked = locked;
63
+ });
64
+ });
65
+ }
27
66
  sync() {
28
67
  if (this.active === '') {
29
68
  return;
@@ -40,10 +79,18 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
40
79
  connectedCallback() {
41
80
  super.connectedCallback();
42
81
  this.addEventListener('helix-select', this.handleSelection);
82
+ this.watcher = new MutationObserver(() => this.lock());
83
+ this.watcher.observe(this, {
84
+ attributeFilter: ['complete', 'current', 'optional'],
85
+ attributes: true,
86
+ subtree: true,
87
+ });
43
88
  }
44
89
  disconnectedCallback() {
45
90
  super.disconnectedCallback();
46
91
  this.removeEventListener('helix-select', this.handleSelection);
92
+ this.watcher?.disconnect();
93
+ this.watcher = undefined;
47
94
  }
48
95
  willUpdate(changed) {
49
96
  super.willUpdate(changed);
@@ -54,6 +101,9 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
54
101
  if (changed.has('active')) {
55
102
  this.sync();
56
103
  }
104
+ if (changed.has('linear') || changed.has('active')) {
105
+ this.lock();
106
+ }
57
107
  }
58
108
  render() {
59
109
  return html `<slot @slotchange=${this.handleSlotChange}></slot>`;
@@ -62,6 +112,9 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
62
112
  __decorate([
63
113
  property()
64
114
  ], HelixHeadlessSteps.prototype, "label", void 0);
115
+ __decorate([
116
+ property({ reflect: true, type: Boolean })
117
+ ], HelixHeadlessSteps.prototype, "linear", void 0);
65
118
  __decorate([
66
119
  property({ reflect: true })
67
120
  ], HelixHeadlessSteps.prototype, "orientation", void 0);