@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
@@ -610,6 +610,10 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
610
610
  this.requestUpdate();
611
611
  };
612
612
  }
613
+ static { this.shadowRootOptions = {
614
+ ...HelixElement.shadowRootOptions,
615
+ slotAssignment: 'manual',
616
+ }; }
613
617
  get box() {
614
618
  const viewport = this.renderRoot?.querySelector('[part="viewport"]');
615
619
  return {
@@ -1530,6 +1534,12 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1530
1534
  updated(changed) {
1531
1535
  this.keying = undefined;
1532
1536
  super.updated(changed);
1537
+ this.assign();
1538
+ const edge = this.focusedEdge;
1539
+ this.focusedEdge = undefined;
1540
+ if (edge && (!edge.target.isConnected || edge.target.getAttribute('data-edge') !== edge.key)) {
1541
+ this.focusStop(edge.key);
1542
+ }
1533
1543
  if (this.holding && !this.held) {
1534
1544
  this.letGo();
1535
1545
  }
@@ -1661,6 +1671,11 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1661
1671
  willUpdate(changed) {
1662
1672
  this.keying = this.keyAll();
1663
1673
  super.willUpdate(changed);
1674
+ const focused = this.ownerDocument?.activeElement === this
1675
+ ? this.renderRoot.activeElement
1676
+ : undefined;
1677
+ const key = focused?.getAttribute?.('data-edge');
1678
+ this.focusedEdge = focused && key ? { key, target: focused } : undefined;
1664
1679
  const drawn = extent(this.nodes);
1665
1680
  const origin = { x: drawn.x - MARGIN, y: drawn.y - MARGIN };
1666
1681
  if (this.origin && (origin.x !== this.origin.x || origin.y !== this.origin.y)) {
@@ -1732,9 +1747,6 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1732
1747
  });
1733
1748
  }
1734
1749
  renderEdges(origin, box) {
1735
- const elements = new Map(this.edgeElements.map((child) => [this.keyOfElement(child), child]));
1736
- const stop = this.stop !== undefined && this.keyNamed(this.stop) === this.stop ? this.stop : undefined;
1737
- const pickedKeys = new Set(this.picked);
1738
1750
  return html `<svg
1739
1751
  part="edges"
1740
1752
  role="none"
@@ -1755,46 +1767,114 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1755
1767
  <path part="arrow" d="M0 0 L8 4 L0 8 z" />
1756
1768
  </marker>
1757
1769
  </defs>
1758
- ${this.edges.map((edge) => {
1759
- const key = this.keyOf(edge);
1760
- const element = elements.get(key);
1761
- const drawn = this.geometryOf(edge, origin);
1762
- const lifted = this.linking?.edge === key && !this.linking.pressed;
1763
- if (!drawn || (lifted && this.linking?.pointer !== undefined)) {
1764
- return nothing;
1765
- }
1766
- const { from, to } = drawn;
1767
- const ends = heads(edge);
1768
- return svg `<path
1769
- part="edge-target"
1770
- aria-description=${[element?.tone ? this.toneLabels[element.tone] : undefined, edge.label].filter(Boolean).join(', ') || nothing}
1771
- aria-label=${fillTemplate(this.connectionLabel, { from: this.textOf(from), to: this.textOf(to) })}
1772
- d=${drawn.d}
1773
- data-edge=${key}
1774
- fill="none"
1775
- aria-keyshortcuts="Enter F2 C Delete"
1776
- role="button"
1777
- tabindex=${key === stop ? '0' : '-1'}
1778
- />
1779
- ${lifted
1780
- ? nothing
1781
- : svg `<path
1782
- part="edge"
1783
- aria-hidden="true"
1784
- d=${drawn.d}
1785
- data-selected=${pickedKeys.has(key) ? '' : nothing}
1786
- data-tone=${element?.tone ?? nothing}
1787
- data-variant=${element?.variant ?? nothing}
1788
- fill="none"
1789
- marker-end=${ends.end ? `url(#${ARROW})` : nothing}
1790
- marker-start=${ends.start ? `url(#${ARROW})` : nothing}
1791
- style=${custom(element, '--canvas-edge-color') ? `--_canvas-edge-color: ${custom(element, '--canvas-edge-color')}` : nothing}
1792
- />`}
1793
- `;
1794
- })}
1795
1770
  ${this.renderDraft(origin)}
1796
1771
  </svg>`;
1797
1772
  }
1773
+ renderStops(origin, box) {
1774
+ const elements = new Map(this.edgeElements.map((child) => [this.keyOfElement(child), child]));
1775
+ const stop = this.stop !== undefined && this.keyNamed(this.stop) === this.stop ? this.stop : undefined;
1776
+ const picked = new Set(this.picked);
1777
+ const groups = [];
1778
+ for (const one of this.walk) {
1779
+ if (one.kind === 'node') {
1780
+ groups.push({ edges: [], key: one.key });
1781
+ }
1782
+ else {
1783
+ groups.at(-1)?.edges.push(one.edge);
1784
+ }
1785
+ }
1786
+ return groups.map(({ edges }, at) => html `<slot name=${`card-${at}`}></slot>${edges.length === 0
1787
+ ? nothing
1788
+ : html `<svg
1789
+ part="edges"
1790
+ role="none"
1791
+ viewBox=${`0 0 ${box.width} ${box.height}`}
1792
+ width=${box.width}
1793
+ height=${box.height}
1794
+ >
1795
+ ${edges.map((edge) => this.renderEdge(edge, origin, elements, stop, picked))}
1796
+ </svg>`}`);
1797
+ }
1798
+ renderEdge(edge, origin, elements, stop, pickedKeys) {
1799
+ const key = this.keyOf(edge);
1800
+ const element = elements.get(key);
1801
+ const drawn = this.geometryOf(edge, origin);
1802
+ const lifted = this.linking?.edge === key && !this.linking.pressed;
1803
+ if (!drawn || (lifted && this.linking?.pointer !== undefined)) {
1804
+ return nothing;
1805
+ }
1806
+ const { from, to } = drawn;
1807
+ const ends = heads(edge);
1808
+ return svg `<path
1809
+ part="edge-target"
1810
+ aria-description=${[element?.tone ? this.toneLabels[element.tone] : undefined, edge.label].filter(Boolean).join(', ') || nothing}
1811
+ aria-label=${fillTemplate(this.connectionLabel, { from: this.textOf(from), to: this.textOf(to) })}
1812
+ d=${drawn.d}
1813
+ data-edge=${key}
1814
+ fill="none"
1815
+ aria-keyshortcuts="Enter F2 C Delete"
1816
+ role="button"
1817
+ tabindex=${key === stop ? '0' : '-1'}
1818
+ />
1819
+ ${lifted
1820
+ ? nothing
1821
+ : svg `<path
1822
+ part="edge"
1823
+ aria-hidden="true"
1824
+ d=${drawn.d}
1825
+ data-selected=${pickedKeys.has(key) ? '' : nothing}
1826
+ data-tone=${element?.tone ?? nothing}
1827
+ data-variant=${element?.variant ?? nothing}
1828
+ fill="none"
1829
+ marker-end=${ends.end ? `url(#${ARROW})` : nothing}
1830
+ marker-start=${ends.start ? `url(#${ARROW})` : nothing}
1831
+ style=${custom(element, '--canvas-edge-color') ? `--_canvas-edge-color: ${custom(element, '--canvas-edge-color')}` : nothing}
1832
+ />`}
1833
+ `;
1834
+ }
1835
+ assign() {
1836
+ if (!this.childrenKnown) {
1837
+ return;
1838
+ }
1839
+ const cards = new Map();
1840
+ for (const card of this.cards) {
1841
+ if (!cards.has(card.name)) {
1842
+ cards.set(card.name, card);
1843
+ }
1844
+ }
1845
+ const placed = new Map();
1846
+ for (const [at, one] of this.walk.filter((stop) => stop.kind === 'node').entries()) {
1847
+ const card = cards.get(one.key);
1848
+ if (card) {
1849
+ placed.set(card, `card-${at}`);
1850
+ }
1851
+ }
1852
+ if (this.assignsByName) {
1853
+ for (const card of this.cards) {
1854
+ this.route(card, placed.get(card));
1855
+ }
1856
+ return;
1857
+ }
1858
+ const root = this.renderRoot;
1859
+ const rest = [];
1860
+ const named = new Map();
1861
+ for (const child of this.knownChildren) {
1862
+ const name = placed.get(child) ?? child.getAttribute('slot') ?? undefined;
1863
+ if (name === undefined) {
1864
+ rest.push(child);
1865
+ }
1866
+ else {
1867
+ named.set(name, [...(named.get(name) ?? []), child]);
1868
+ }
1869
+ }
1870
+ for (const slot of root?.querySelectorAll?.('slot') ?? []) {
1871
+ const wanted = slot.name === '' ? rest : (named.get(slot.name) ?? []);
1872
+ const had = slot.assignedNodes();
1873
+ if (had.length !== wanted.length || had.some((one, at) => one !== wanted[at])) {
1874
+ slot.assign?.(...wanted);
1875
+ }
1876
+ }
1877
+ }
1798
1878
  renderLabels(origin) {
1799
1879
  return this.edges.map((edge) => {
1800
1880
  const lifted = this.linking?.edge === this.keyOf(edge) && !this.linking.pressed;
@@ -1903,6 +1983,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1903
1983
  @focusin=${this.handleFocusIn}
1904
1984
  >
1905
1985
  ${this.renderEdges(origin, box)} ${this.renderLabels(origin)}
1986
+ ${this.renderStops(origin, box)}
1906
1987
  <slot @slotchange=${this.handleSlotChange}></slot>
1907
1988
  ${this.renderEnds(origin)}
1908
1989
  ${this.renderEditor(origin)}
@@ -29,6 +29,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
29
29
  * and the last. Without `loop`, Previous on the first slide and Next on the
30
30
  * last are marked `aria-disabled`.
31
31
  *
32
+ * **`mouse-drag` lets a mouse drag the track**, which otherwise only a touch
33
+ * screen, a trackpad, or the scroll wheel moves. The drag follows the pointer
34
+ * with snapping off and settles on the nearest slide when it is let go, and
35
+ * that move is announced and refusable like a press of Next. A link in a slide
36
+ * is not followed at the end of a drag.
37
+ *
38
+ * The glyphs on Previous and Next can be replaced by slotting an icon of your
39
+ * own into `previous-icon` and `next-icon`.
40
+ *
32
41
  * @since 0.1.0
33
42
  * @status stable
34
43
  * @category layout
@@ -36,6 +45,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
36
45
  * @documentation https://fusion.dev/helix/components/carousel
37
46
  *
38
47
  * @slot - The slides.
48
+ * @slot next-icon - The glyph on the control that moves to the next slide. Defaults to a caret.
49
+ * @slot previous-icon - The glyph on the control that moves to the previous slide. Defaults to a caret.
39
50
  *
40
51
  * @event {HelixActivateEvent} helix-activate - Fired after the slide in view changes, with the new index as a string in `detail.value`.
41
52
  * @event {HelixSelectEvent} helix-select - Fired before the slide in view changes, with the requested index. Cancelable: cancel it to keep the current slide. Nothing moves, and a drag returns to the slide the index still holds. See `handleScroll`.
@@ -59,6 +70,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
59
70
  * @cssproperty --carousel-per-view - How many slides go across. The component writes it onto the track, so your stylesheet divides the track by the same number the component counts positions from. Read this property to follow `per-view`, because attribute selectors cannot cover every possible count. Under `per-view="auto"` the component writes nothing and your stylesheet declares it, so the same name works in both modes. See `perView`.
60
71
  * @cssproperty --carousel-run-end - How far short of a whole pixel the run ends, set on the last slide only. Your stylesheet applies it as `margin-inline-end`. See `runEnd`.
61
72
  * @cssproperty --carousel-slide-min - The narrowest a slide may be. `per-view="auto"` uses it to fit slides into the track. It must be a length, because the stylesheet divides by it. When unset, it is the whole track, which shows one slide across.
73
+ *
74
+ * @cssstate dragging - While a mouse drags the track, under `mouse-drag`.
62
75
  */
63
76
  export declare class HelixHeadlessCarousel extends HelixElement {
64
77
  /**
@@ -154,6 +167,14 @@ export declare class HelixHeadlessCarousel extends HelixElement {
154
167
  * Whether moving past either end wraps to the other.
155
168
  */
156
169
  loop: boolean;
170
+ /**
171
+ * Whether a mouse can drag the track from slide to slide.
172
+ *
173
+ * Off unless asked for, because a press on a slide is then the start of a
174
+ * drag, and text in a slide can no longer be selected with the mouse. A drag
175
+ * of a few pixels or less is still a press.
176
+ */
177
+ mouseDrag: boolean;
157
178
  /**
158
179
  * The name of the control that moves to the next slide.
159
180
  */
@@ -246,6 +267,12 @@ export declare class HelixHeadlessCarousel extends HelixElement {
246
267
  * The name of the control that moves to the previous slide.
247
268
  */
248
269
  previousLabel: string;
270
+ /**
271
+ * The word screen readers announce in place of the carousel's role, set as
272
+ * `aria-roledescription`. Translate it with the rest of your text, or empty
273
+ * it to leave the role as `group`.
274
+ */
275
+ roleLabel: string;
249
276
  /**
250
277
  * The name of each slide, with `{index}` and `{count}` filled in.
251
278
  */
@@ -386,6 +413,25 @@ export declare class HelixHeadlessCarousel extends HelixElement {
386
413
  * in exactly as the inline one did.
387
414
  */
388
415
  private handed;
416
+ /**
417
+ * The track's inline style: the count above, and snapping off while a mouse
418
+ * drags it.
419
+ *
420
+ * **Inline, because the drag does not work without it.** A track that snaps
421
+ * puts every scroll the drag asks for back on the nearest slide, so the
422
+ * slides jump from one to the next under the pointer. A stylesheet cannot be
423
+ * relied on to turn snapping off, since the headless tier has none, so the
424
+ * component says it for the length of the drag and no longer.
425
+ *
426
+ * **And `!important`, because a document rule beats a shadow root's inline
427
+ * style.** The style attribute is in the shadow tree's context, and a
428
+ * consumer's `::part(track)` rule is in the document's, which wins every
429
+ * normal declaration. Measured in Chromium on a headless track snapping from
430
+ * the document: the inline `none` was there and the computed value stayed
431
+ * `x mandatory`, and the drag never moved the track. An important
432
+ * declaration sorts the other way, the inner context first.
433
+ */
434
+ private trackStyle;
389
435
  /** What the box resolved to, and `undefined` until it has been laid out. */
390
436
  private fitted;
391
437
  /** Watches the pane a fitted track laid out, while `auto` is what was asked. */
@@ -425,6 +471,8 @@ export declare class HelixHeadlessCarousel extends HelixElement {
425
471
  private get panes();
426
472
  /** The slides, in light-DOM order. Empty on the server, which has no tree. */
427
473
  get slides(): HTMLElement[];
474
+ /** What a consumer slotted into one of the arrows' glyph slots. */
475
+ private iconsFor;
428
476
  /**
429
477
  * The arrows move between indicators, which is the tablist pattern.
430
478
  *
@@ -509,6 +557,38 @@ export declare class HelixHeadlessCarousel extends HelixElement {
509
557
  */
510
558
  private settling;
511
559
  private readonly handleTake;
560
+ /** Whether a mouse is dragging the track. */
561
+ private dragging;
562
+ /** A mouse pressed on the track under `mouse-drag`, from where it pressed. */
563
+ private pull;
564
+ /** Takes the press, and under `mouse-drag` the start of a drag. */
565
+ private readonly handlePress;
566
+ /**
567
+ * Moves the track with the pointer.
568
+ *
569
+ * Not until the pointer has moved a few pixels, so a press on a link or a
570
+ * button in a slide stays a press. Instant scrolls, since a smooth one would
571
+ * trail the pointer.
572
+ */
573
+ private readonly handlePull;
574
+ /**
575
+ * Lets go of the track, and settles on the slide nearest to where it was let
576
+ * go.
577
+ *
578
+ * Through the same request as a press of Next, so the move is refusable,
579
+ * sends `helix-activate`, and is announced. A refusal, or a drag that ends on
580
+ * the slide it began on, goes back to that slide once snapping is on again.
581
+ */
582
+ private readonly handleLetGo;
583
+ /**
584
+ * Keeps the click that ends a drag from following a link in the slide.
585
+ *
586
+ * Once, in capture on the track, and given up a moment later where no click
587
+ * comes, which is a drag let go off the track.
588
+ */
589
+ private swallowClick;
590
+ /** Keeps a mouse drag from picking up a link or an image under `mouse-drag`. */
591
+ private readonly handleDragStart;
512
592
  /**
513
593
  * Stops rotation for good.
514
594
  *
@@ -705,6 +785,16 @@ export declare class HelixHeadlessCarousel extends HelixElement {
705
785
  * server, which knows no slides and so no ends.
706
786
  */
707
787
  private atEnd;
788
+ /**
789
+ * The glyph on Previous: the `previous-icon` slot, holding a caret until
790
+ * something is slotted. A tier above overrides this to draw its own caret.
791
+ */
792
+ protected renderPreviousMark(): TemplateResult;
793
+ /**
794
+ * The glyph on Next: the `next-icon` slot, holding a caret until something
795
+ * is slotted. A tier above overrides this to draw its own caret.
796
+ */
797
+ protected renderNextMark(): TemplateResult;
708
798
  protected render(): TemplateResult;
709
799
  }
710
800
  declare global {
@@ -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 { customElement, fillTemplate, HelixActivateEvent, HelixElement, HelixSelectEvent, hostAria, inlineStep, isRtl, requestChange, } from '@fusion.dev/helix-core';
7
+ import { capturePointer, customElement, fillTemplate, HelixActivateEvent, HelixElement, HelixSelectEvent, hostAria, inlineStep, isRtl, requestChange, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft, caretRight, play, stop } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -18,6 +18,8 @@ const COUNT_OR_AUTO = {
18
18
  },
19
19
  toAttribute: (value) => value === undefined ? null : String(value),
20
20
  };
21
+ const ICON_SLOTS = ['previous-icon', 'next-icon'];
22
+ const DRAG_SLOP = 4;
21
23
  let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
22
24
  constructor() {
23
25
  super(...arguments);
@@ -31,9 +33,11 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
31
33
  this.said = '';
32
34
  this.label = 'Carousel';
33
35
  this.loop = false;
36
+ this.mouseDrag = false;
34
37
  this.nextLabel = 'Next slide';
35
38
  this.rotating = false;
36
39
  this.previousLabel = 'Previous slide';
40
+ this.roleLabel = 'carousel';
37
41
  this.slideLabel = '{index} of {count}';
38
42
  this.slideRoleLabel = 'slide';
39
43
  this.startLabel = 'Start slide rotation';
@@ -75,6 +79,68 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
75
79
  this.handleTake = () => {
76
80
  this.settling = false;
77
81
  };
82
+ this.dragging = false;
83
+ this.handlePress = (event) => {
84
+ this.handleTake();
85
+ const track = this.partNamed('track');
86
+ if (!this.mouseDrag || event.pointerType !== 'mouse' || event.button !== 0 || !track) {
87
+ return;
88
+ }
89
+ this.pull = { left: track.scrollLeft, top: track.scrollTop, x: event.clientX, y: event.clientY };
90
+ };
91
+ this.handlePull = (event) => {
92
+ const pull = this.pull;
93
+ const track = this.partNamed('track');
94
+ if (!pull || !track) {
95
+ return;
96
+ }
97
+ if ((event.buttons & 1) === 0) {
98
+ this.pull = undefined;
99
+ return;
100
+ }
101
+ const across = event.clientX - pull.x;
102
+ const down = event.clientY - pull.y;
103
+ if (!this.dragging) {
104
+ if (Math.hypot(across, down) < DRAG_SLOP) {
105
+ return;
106
+ }
107
+ this.dragging = true;
108
+ track.style.setProperty('scroll-snap-type', 'none', 'important');
109
+ capturePointer(track, event.pointerId);
110
+ }
111
+ track.scrollTo(this.orientation === 'vertical'
112
+ ? { behavior: 'instant', top: pull.top - down }
113
+ : { behavior: 'instant', left: pull.left - across });
114
+ };
115
+ this.handleLetGo = () => {
116
+ if (!this.pull) {
117
+ return;
118
+ }
119
+ this.pull = undefined;
120
+ if (!this.dragging) {
121
+ return;
122
+ }
123
+ this.swallowClick();
124
+ const nearest = this.nearest();
125
+ this.dragging = false;
126
+ this.showing = undefined;
127
+ const moved = nearest !== undefined &&
128
+ nearest !== this.index &&
129
+ requestChange({
130
+ apply: (value) => this.applyIndex(value),
131
+ event: (value) => new HelixSelectEvent({ value: String(value) }),
132
+ host: this,
133
+ next: nearest,
134
+ });
135
+ if (!moved) {
136
+ this.updateComplete.then(() => this.scrollToIndex());
137
+ }
138
+ };
139
+ this.handleDragStart = (event) => {
140
+ if (this.mouseDrag) {
141
+ event.preventDefault();
142
+ }
143
+ };
78
144
  this.handleTrackScroll = () => {
79
145
  if (this.settling || this.scrollFrame) {
80
146
  return;
@@ -92,6 +158,9 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
92
158
  this.settling = false;
93
159
  return;
94
160
  }
161
+ if (this.dragging) {
162
+ return;
163
+ }
95
164
  this.showing = undefined;
96
165
  const nearest = this.nearest();
97
166
  if (nearest === undefined) {
@@ -169,6 +238,14 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
169
238
  handed() {
170
239
  return this.fits ? nothing : `--carousel-per-view: ${this.clamped}`;
171
240
  }
241
+ trackStyle() {
242
+ const handed = this.handed();
243
+ if (!this.dragging) {
244
+ return handed;
245
+ }
246
+ const off = 'scroll-snap-type: none !important';
247
+ return handed === nothing ? off : `${handed}; ${off}`;
248
+ }
172
249
  watchPane() {
173
250
  if (!this.fits || typeof ResizeObserver === 'undefined') {
174
251
  this.sizes?.disconnect();
@@ -193,7 +270,10 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
193
270
  return [...(this.renderRoot?.querySelectorAll?.('[part="slide"]') ?? [])];
194
271
  }
195
272
  get slides() {
196
- return this.knownChildren.filter((child) => child instanceof HTMLElement);
273
+ return this.knownChildren.filter((child) => child instanceof HTMLElement && !ICON_SLOTS.includes(child.slot));
274
+ }
275
+ iconsFor(name) {
276
+ return this.knownChildren.filter((child) => child.slot === name);
197
277
  }
198
278
  applyIndex(value) {
199
279
  const previous = this.index;
@@ -251,6 +331,18 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
251
331
  this.settling = true;
252
332
  track.scrollTo({ behavior: still ? 'instant' : 'smooth', ...at });
253
333
  }
334
+ swallowClick() {
335
+ const track = this.partNamed('track');
336
+ if (!track) {
337
+ return;
338
+ }
339
+ const swallow = (event) => {
340
+ event.preventDefault();
341
+ event.stopPropagation();
342
+ };
343
+ track.addEventListener('click', swallow, { capture: true, once: true });
344
+ setTimeout(() => track.removeEventListener('click', swallow, { capture: true }));
345
+ }
254
346
  stop() {
255
347
  this.stopped = true;
256
348
  this.rotating = false;
@@ -324,7 +416,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
324
416
  this.fitted = undefined;
325
417
  }
326
418
  hostAria(this.internals, { label: this.label, role: 'group' });
327
- this.internals.ariaRoleDescription = 'carousel';
419
+ this.internals.ariaRoleDescription = this.roleLabel || null;
328
420
  }
329
421
  firstUpdated(changed) {
330
422
  super.firstUpdated(changed);
@@ -342,6 +434,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
342
434
  this.holdIndex();
343
435
  this.watchPane();
344
436
  this.assign();
437
+ this.toggleState('dragging', this.dragging);
345
438
  }
346
439
  assign() {
347
440
  if (this.assignsByName) {
@@ -350,6 +443,11 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
350
443
  });
351
444
  return;
352
445
  }
446
+ for (const name of ICON_SLOTS) {
447
+ this.renderRoot
448
+ ?.querySelector?.(`slot[name="${name}"]`)
449
+ ?.assign?.(...this.iconsFor(name));
450
+ }
353
451
  const panes = this.renderRoot?.querySelectorAll?.('[part="slide"] slot');
354
452
  if (!panes) {
355
453
  return;
@@ -372,6 +470,20 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
372
470
  }
373
471
  return side === 'start' ? this.index <= 0 : this.index >= this.positions - 1;
374
472
  }
473
+ renderPreviousMark() {
474
+ return html `<slot name="previous-icon"
475
+ ><svg part="previous-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
476
+ ${unsafeSVG(caretLeft.content)}
477
+ </svg></slot
478
+ >`;
479
+ }
480
+ renderNextMark() {
481
+ return html `<slot name="next-icon"
482
+ ><svg part="next-mark" data-mirror fill="currentColor" viewBox=${caretRight.viewBox} aria-hidden="true">
483
+ ${unsafeSVG(caretRight.content)}
484
+ </svg></slot
485
+ >`;
486
+ }
375
487
  render() {
376
488
  const rotating = this.rotating;
377
489
  const last = this.slides.length - 1;
@@ -401,9 +513,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
401
513
  aria-disabled=${this.atEnd('start') ? 'true' : nothing}
402
514
  @click=${() => this.goTo(this.index - 1)}
403
515
  >
404
- <svg part="previous-mark" data-mirror fill="currentColor" viewBox=${caretLeft.viewBox} aria-hidden="true">
405
- ${unsafeSVG(caretLeft.content)}
406
- </svg>
516
+ ${this.renderPreviousMark()}
407
517
  </button>
408
518
  <button
409
519
  part="next"
@@ -412,25 +522,28 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
412
522
  aria-disabled=${this.atEnd('end') ? 'true' : nothing}
413
523
  @click=${() => this.goTo(this.index + 1)}
414
524
  >
415
- <svg part="next-mark" data-mirror fill="currentColor" viewBox=${caretRight.viewBox} aria-hidden="true">
416
- ${unsafeSVG(caretRight.content)}
417
- </svg>
525
+ ${this.renderNextMark()}
418
526
  </button>`}
419
527
  </div>
420
528
  <div
421
529
  data-scroller
422
530
  part="track"
423
- style=${this.handed()}
531
+ style=${this.trackStyle()}
424
532
  @scroll=${this.handleTrackScroll}
425
533
  @scrollend=${this.handleScroll}
426
- @pointerdown=${this.handleTake}
534
+ @pointerdown=${this.handlePress}
535
+ @pointermove=${this.handlePull}
536
+ @pointerup=${this.handleLetGo}
537
+ @pointercancel=${this.handleLetGo}
538
+ @lostpointercapture=${this.handleLetGo}
539
+ @dragstart=${this.handleDragStart}
427
540
  @touchstart=${this.handleTake}
428
541
  @wheel=${this.handleTake}
429
542
  >
430
543
  ${this.slides.map((_, index) => html `<div
431
544
  part="slide"
432
545
  role="group"
433
- aria-roledescription=${this.slideRoleLabel}
546
+ aria-roledescription=${this.slideRoleLabel || nothing}
434
547
  aria-label=${fillTemplate(this.slideLabel, { count: this.slides.length, index: index + 1 })}
435
548
  style=${index === last ? this.runEnd() : nothing}
436
549
  >
@@ -488,6 +601,9 @@ __decorate([
488
601
  __decorate([
489
602
  property({ type: Boolean })
490
603
  ], HelixHeadlessCarousel.prototype, "loop", void 0);
604
+ __decorate([
605
+ property({ attribute: 'mouse-drag', reflect: true, type: Boolean })
606
+ ], HelixHeadlessCarousel.prototype, "mouseDrag", void 0);
491
607
  __decorate([
492
608
  property({ attribute: 'next-label' })
493
609
  ], HelixHeadlessCarousel.prototype, "nextLabel", void 0);
@@ -503,6 +619,9 @@ __decorate([
503
619
  __decorate([
504
620
  property({ attribute: 'previous-label' })
505
621
  ], HelixHeadlessCarousel.prototype, "previousLabel", void 0);
622
+ __decorate([
623
+ property({ attribute: 'role-label' })
624
+ ], HelixHeadlessCarousel.prototype, "roleLabel", void 0);
506
625
  __decorate([
507
626
  property({ attribute: 'slide-label' })
508
627
  ], HelixHeadlessCarousel.prototype, "slideLabel", void 0);
@@ -518,6 +637,9 @@ __decorate([
518
637
  __decorate([
519
638
  state()
520
639
  ], HelixHeadlessCarousel.prototype, "fitted", void 0);
640
+ __decorate([
641
+ state()
642
+ ], HelixHeadlessCarousel.prototype, "dragging", void 0);
521
643
  __decorate([
522
644
  state()
523
645
  ], HelixHeadlessCarousel.prototype, "showing", void 0);