@fusion.dev/helix-headless 0.143.0 → 0.144.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 (191) hide show
  1. package/custom-elements.json +8551 -375
  2. package/dist/area-chart/root/area-chart-root.d.ts +12 -4
  3. package/dist/area-chart/root/area-chart-root.js +24 -7
  4. package/dist/avatar/root/avatar-root.d.ts +14 -0
  5. package/dist/avatar/root/avatar-root.js +23 -9
  6. package/dist/badge/root/badge-root.d.ts +11 -7
  7. package/dist/badge/root/badge-root.js +1 -1
  8. package/dist/bar-chart/root/bar-chart-root.d.ts +12 -3
  9. package/dist/bar-chart/root/bar-chart-root.js +17 -10
  10. package/dist/button/root/button-root.d.ts +47 -1
  11. package/dist/button/root/button-root.js +38 -4
  12. package/dist/callout/root/callout-root.d.ts +18 -1
  13. package/dist/callout/root/callout-root.js +15 -4
  14. package/dist/card/root/card-root.d.ts +30 -1
  15. package/dist/card/root/card-root.js +38 -5
  16. package/dist/checkbox/root/checkbox-root.js +2 -0
  17. package/dist/checkbox-group/root/checkbox-group-root.d.ts +53 -3
  18. package/dist/checkbox-group/root/checkbox-group-root.js +71 -22
  19. package/dist/code-block/lines.d.ts +13 -0
  20. package/dist/code-block/lines.js +2 -0
  21. package/dist/code-block/root/code-block-root.d.ts +30 -0
  22. package/dist/code-block/root/code-block-root.js +23 -2
  23. package/dist/color-input/root/color-input-root.d.ts +34 -1
  24. package/dist/color-input/root/color-input-root.js +23 -4
  25. package/dist/color-picker/channels.d.ts +26 -0
  26. package/dist/color-picker/channels.js +84 -0
  27. package/dist/color-picker/root/color-picker-root.d.ts +81 -5
  28. package/dist/color-picker/root/color-picker-root.js +103 -10
  29. package/dist/combobox/root/combobox-root.d.ts +38 -3
  30. package/dist/combobox/root/combobox-root.js +64 -2
  31. package/dist/command/root/command-root.d.ts +30 -4
  32. package/dist/command/root/command-root.js +33 -3
  33. package/dist/copy-button/from.d.ts +22 -0
  34. package/dist/copy-button/from.js +28 -0
  35. package/dist/copy-button/root/copy-button-root.d.ts +27 -1
  36. package/dist/copy-button/root/copy-button-root.js +43 -7
  37. package/dist/date-input/root/date-input-root.d.ts +73 -4
  38. package/dist/date-input/root/date-input-root.js +93 -43
  39. package/dist/date-picker/root/date-picker-root.d.ts +145 -1
  40. package/dist/date-picker/root/date-picker-root.js +239 -48
  41. package/dist/dialog/description.d.ts +9 -0
  42. package/dist/dialog/description.js +14 -0
  43. package/dist/dialog/root/dialog-root.d.ts +64 -1
  44. package/dist/dialog/root/dialog-root.js +49 -23
  45. package/dist/donut-chart/root/donut-chart-root.d.ts +25 -4
  46. package/dist/donut-chart/root/donut-chart-root.js +21 -6
  47. package/dist/drawer/root/drawer-root.d.ts +63 -2
  48. package/dist/drawer/root/drawer-root.js +95 -19
  49. package/dist/drawer/swipe-start.d.ts +9 -0
  50. package/dist/drawer/swipe-start.js +12 -0
  51. package/dist/element-events.generated.js +8 -5
  52. package/dist/empty/root/empty-root.d.ts +1 -1
  53. package/dist/file-input/root/file-input-root.d.ts +47 -0
  54. package/dist/file-input/root/file-input-root.js +60 -22
  55. package/dist/file-input/screen.d.ts +35 -0
  56. package/dist/file-input/screen.js +49 -0
  57. package/dist/filter-input/root/filter-input-root.d.ts +38 -4
  58. package/dist/filter-input/root/filter-input-root.js +67 -3
  59. package/dist/filter-input/suggest.d.ts +17 -0
  60. package/dist/filter-input/suggest.js +23 -0
  61. package/dist/format-date/options.d.ts +33 -0
  62. package/dist/format-date/options.js +42 -0
  63. package/dist/format-date/root/format-date-root.d.ts +51 -0
  64. package/dist/format-date/root/format-date-root.js +58 -38
  65. package/dist/heatmap/root/heatmap-root.d.ts +23 -6
  66. package/dist/heatmap/root/heatmap-root.js +24 -13
  67. package/dist/icon/index.d.ts +2 -0
  68. package/dist/icon/index.js +2 -0
  69. package/dist/icon/registry.d.ts +37 -0
  70. package/dist/icon/registry.js +83 -0
  71. package/dist/icon/root/icon-root.d.ts +30 -4
  72. package/dist/icon/root/icon-root.js +41 -6
  73. package/dist/icon/root/index.d.ts +2 -0
  74. package/dist/icon/root/index.js +2 -0
  75. package/dist/icon/set.d.ts +1 -0
  76. package/dist/icon/set.js +3 -0
  77. package/dist/image/root/image-root.d.ts +14 -0
  78. package/dist/image/root/image-root.js +15 -2
  79. package/dist/input/root/input-root.d.ts +11 -0
  80. package/dist/input/root/input-root.js +15 -0
  81. package/dist/kbd/root/kbd-root.d.ts +20 -13
  82. package/dist/kbd/root/kbd-root.js +35 -37
  83. package/dist/kbd/shortcut.d.ts +18 -0
  84. package/dist/kbd/shortcut.js +35 -0
  85. package/dist/legend/hidden-series.d.ts +8 -0
  86. package/dist/legend/hidden-series.js +13 -0
  87. package/dist/legend/root/legend-root.d.ts +56 -0
  88. package/dist/legend/root/legend-root.js +54 -5
  89. package/dist/lightbox/item/lightbox-item.d.ts +15 -0
  90. package/dist/lightbox/item/lightbox-item.js +10 -1
  91. package/dist/lightbox/root/lightbox-root.d.ts +61 -1
  92. package/dist/lightbox/root/lightbox-root.js +94 -4
  93. package/dist/line-chart/root/line-chart-root.d.ts +11 -3
  94. package/dist/line-chart/root/line-chart-root.js +19 -5
  95. package/dist/listbox/range.d.ts +17 -0
  96. package/dist/listbox/range.js +5 -0
  97. package/dist/listbox/root/listbox-root.d.ts +74 -3
  98. package/dist/listbox/root/listbox-root.js +87 -9
  99. package/dist/loader/busy.d.ts +17 -0
  100. package/dist/loader/busy.js +24 -0
  101. package/dist/loader/root/loader-root.d.ts +37 -0
  102. package/dist/loader/root/loader-root.js +52 -3
  103. package/dist/meter/root/meter-root.d.ts +7 -0
  104. package/dist/meter/root/meter-root.js +6 -3
  105. package/dist/notifications/feed-keys.d.ts +10 -0
  106. package/dist/notifications/feed-keys.js +14 -0
  107. package/dist/notifications/notification/notification.d.ts +17 -0
  108. package/dist/notifications/notification/notification.js +21 -2
  109. package/dist/notifications/root/notifications-root.d.ts +42 -0
  110. package/dist/notifications/root/notifications-root.js +60 -1
  111. package/dist/number-input/root/number-input-root.d.ts +47 -0
  112. package/dist/number-input/root/number-input-root.js +58 -22
  113. package/dist/object-inspector/root/object-inspector-root.d.ts +1 -0
  114. package/dist/object-inspector/root/object-inspector-root.js +1 -1
  115. package/dist/option/root/option-root.d.ts +54 -5
  116. package/dist/option/root/option-root.js +34 -5
  117. package/dist/pin-input/format.d.ts +7 -0
  118. package/dist/pin-input/format.js +16 -0
  119. package/dist/pin-input/root/pin-input-root.d.ts +21 -0
  120. package/dist/pin-input/root/pin-input-root.js +29 -2
  121. package/dist/popover/root/popover-root.d.ts +62 -4
  122. package/dist/popover/root/popover-root.js +53 -5
  123. package/dist/progress/root/progress-root.d.ts +7 -0
  124. package/dist/progress/root/progress-root.js +9 -3
  125. package/dist/qr-code/export.d.ts +31 -0
  126. package/dist/qr-code/export.js +10 -0
  127. package/dist/qr-code/root/qr-code-root.d.ts +26 -0
  128. package/dist/qr-code/root/qr-code-root.js +49 -2
  129. package/dist/radio/group/radio-group.d.ts +23 -0
  130. package/dist/radio/group/radio-group.js +18 -0
  131. package/dist/rating/root/rating-root.d.ts +25 -0
  132. package/dist/rating/root/rating-root.js +45 -3
  133. package/dist/scatter-chart/root/scatter-chart-root.d.ts +17 -3
  134. package/dist/scatter-chart/root/scatter-chart-root.js +25 -7
  135. package/dist/select/root/select-root.d.ts +24 -0
  136. package/dist/select/root/select-root.js +20 -6
  137. package/dist/skeleton/root/skeleton-root.d.ts +25 -0
  138. package/dist/skeleton/root/skeleton-root.js +17 -2
  139. package/dist/slider/root/slider-root.d.ts +39 -0
  140. package/dist/slider/root/slider-root.js +45 -9
  141. package/dist/stat/root/stat-root.d.ts +2 -0
  142. package/dist/stat/root/stat-root.js +1 -1
  143. package/dist/steps/step/step.d.ts +1 -0
  144. package/dist/steps/step/step.js +1 -1
  145. package/dist/switch/root/switch-root.d.ts +12 -2
  146. package/dist/switch/root/switch-root.js +6 -1
  147. package/dist/tag/group/tag-group.d.ts +107 -0
  148. package/dist/tag/group/tag-group.js +157 -0
  149. package/dist/tag/index.d.ts +1 -0
  150. package/dist/tag/index.js +1 -0
  151. package/dist/tag/root/tag-root.d.ts +60 -0
  152. package/dist/tag/root/tag-root.js +74 -18
  153. package/dist/tag/selection.d.ts +10 -0
  154. package/dist/tag/selection.js +12 -0
  155. package/dist/textarea/grow.d.ts +22 -0
  156. package/dist/textarea/grow.js +20 -0
  157. package/dist/textarea/root/textarea-root.d.ts +9 -0
  158. package/dist/textarea/root/textarea-root.js +9 -1
  159. package/dist/time-input/root/time-input-root.d.ts +44 -5
  160. package/dist/time-input/root/time-input-root.js +69 -39
  161. package/dist/time-picker/root/time-picker-root.d.ts +54 -8
  162. package/dist/time-picker/root/time-picker-root.js +121 -91
  163. package/dist/toast/hotkey.d.ts +22 -0
  164. package/dist/toast/hotkey.js +23 -0
  165. package/dist/toast/item/toast-item.d.ts +16 -1
  166. package/dist/toast/item/toast-item.js +41 -13
  167. package/dist/toast/root/toast-root.d.ts +42 -8
  168. package/dist/toast/root/toast-root.js +64 -4
  169. package/dist/toggle/group/toggle-group.d.ts +57 -15
  170. package/dist/toggle/group/toggle-group.js +69 -9
  171. package/dist/toggle/root/toggle-root.d.ts +12 -1
  172. package/dist/toggle/root/toggle-root.js +11 -3
  173. package/dist/tooltip/focus-target.d.ts +6 -0
  174. package/dist/tooltip/focus-target.js +9 -0
  175. package/dist/tooltip/root/tooltip-root.d.ts +42 -17
  176. package/dist/tooltip/root/tooltip-root.js +50 -9
  177. package/dist/tour/naming.d.ts +30 -0
  178. package/dist/tour/naming.js +10 -0
  179. package/dist/tour/root/tour-root.d.ts +35 -0
  180. package/dist/tour/root/tour-root.js +56 -5
  181. package/dist/tour/stop/tour-stop.d.ts +53 -8
  182. package/dist/tour/stop/tour-stop.js +45 -15
  183. package/dist/tour/wait.d.ts +11 -0
  184. package/dist/tour/wait.js +19 -0
  185. package/dist/trend/root/trend-root.d.ts +13 -0
  186. package/dist/trend/root/trend-root.js +14 -9
  187. package/lint.json +1 -1
  188. package/package.json +5 -5
  189. package/types/jsx.d.ts +4862 -254
  190. package/types/svelte.d.ts +1 -0
  191. package/types/vue.d.ts +1149 -67
@@ -5,10 +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
  import { customElement, HelixElement, hostAria, reportRuntime } from '@fusion.dev/helix-core';
8
- import { icons } from '@fusion.dev/helix-icons';
9
8
  import { html, nothing } from 'lit';
10
9
  import { property } from 'lit/decorators.js';
11
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
11
+ import { resolveIcon, watchIcons } from '../registry.js';
12
12
  let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
13
13
  constructor() {
14
14
  super(...arguments);
@@ -16,6 +16,8 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
16
16
  this.mirror = false;
17
17
  this.name = '';
18
18
  this.missed = '';
19
+ this.waiting = false;
20
+ this.arrived = '';
19
21
  }
20
22
  static get observedAttributes() {
21
23
  return [...super.observedAttributes, 'aria-label'];
@@ -26,21 +28,55 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
26
28
  this.requestUpdate();
27
29
  }
28
30
  }
31
+ connectedCallback() {
32
+ super.connectedCallback();
33
+ this.unwatch = watchIcons(() => {
34
+ if (this.icon === undefined && this.name !== '') {
35
+ this.requestUpdate();
36
+ }
37
+ });
38
+ }
39
+ disconnectedCallback() {
40
+ super.disconnectedCallback();
41
+ this.unwatch?.();
42
+ this.unwatch = undefined;
43
+ }
44
+ lookup() {
45
+ const found = this.name === '' ? undefined : resolveIcon(this.name);
46
+ this.waiting = found instanceof Promise;
47
+ if (!(found instanceof Promise)) {
48
+ return found;
49
+ }
50
+ const name = this.name;
51
+ void found.then(() => {
52
+ if (this.name === name && this.isConnected) {
53
+ this.arrived = name;
54
+ this.requestUpdate();
55
+ }
56
+ });
57
+ return undefined;
58
+ }
29
59
  willUpdate(changed) {
30
60
  super.willUpdate(changed);
31
61
  const named = this.label === '' && (this.getAttribute?.('aria-label') ?? '') !== '';
32
62
  hostAria(this.internals, { role: named ? 'img' : '' });
63
+ this.drawn = this.icon ?? this.lookup();
33
64
  }
34
65
  updated(changed) {
35
66
  super.updated(changed);
36
- if (this.icon === undefined && this.name !== '' && !Object.hasOwn(icons, this.name)) {
67
+ if (this.arrived === this.name && this.drawn !== undefined) {
68
+ this.dispatchEvent(new Event('load', { bubbles: false, cancelable: false, composed: false }));
69
+ }
70
+ this.arrived = '';
71
+ if (this.drawn === undefined && this.name !== '' && !this.waiting) {
37
72
  if (this.missed !== this.name) {
38
73
  this.missed = this.name;
39
74
  this.dispatchEvent(new Event('error', { bubbles: false, cancelable: false, composed: false }));
40
75
  reportRuntime({
41
76
  kind: 'unknown-icon',
42
- message: `<${this.localName} name="${this.name}"> names no icon in the Helix set, so it draws ` +
43
- 'nothing. Check the name, or import the glyph and pass it as `icon`.',
77
+ message: `<${this.localName} name="${this.name}"> names no registered icon, so it draws ` +
78
+ 'nothing. Check the name, register the glyph with `registerIcons`, or import it ' +
79
+ 'and pass it as `icon`.',
44
80
  tag: this.localName,
45
81
  });
46
82
  }
@@ -50,8 +86,7 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
50
86
  }
51
87
  }
52
88
  render() {
53
- const named = Object.hasOwn(icons, this.name) ? icons[this.name] : undefined;
54
- const icon = this.icon ?? named;
89
+ const icon = this.drawn;
55
90
  if (!icon) {
56
91
  return nothing;
57
92
  }
@@ -0,0 +1,2 @@
1
+ export { type IconResolver, registerIconLibrary, registerIcons, resolveIcon } from '../registry.js';
2
+ export * from './icon-root.js';
@@ -0,0 +1,2 @@
1
+ export { registerIconLibrary, registerIcons, resolveIcon } from '../registry.js';
2
+ export * from './icon-root.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { icons } from '@fusion.dev/helix-icons';
2
+ import { registerIcons } from './registry.js';
3
+ registerIcons(icons);
@@ -27,6 +27,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
27
27
  * @csspart fallback - The fallback, shown when the image fails to load.
28
28
  * @csspart image - The image itself.
29
29
  * @csspart placeholder - The placeholder, shown until the image loads.
30
+ *
31
+ * @cssstate error - The image failed to load, and the fallback shows.
32
+ * @cssstate loaded - The image loaded.
30
33
  */
31
34
  export declare class HelixHeadlessImage extends HelixElement {
32
35
  /**
@@ -71,6 +74,14 @@ export declare class HelixHeadlessImage extends HelixElement {
71
74
  * that is not is the exception worth writing down.
72
75
  */
73
76
  loading?: 'eager' | 'lazy';
77
+ /**
78
+ * How long, in milliseconds, the placeholder waits before it shows.
79
+ *
80
+ * Absent or `0` shows it at once. A picture that arrives inside
81
+ * the wait never shows a placeholder at all, so a cached or fast image does
82
+ * not flash one. Radix's `delayMs` on its avatar fallback is the same idea.
83
+ */
84
+ placeholderDelay: number;
74
85
  /**
75
86
  * How much of the referring URL to send with the request.
76
87
  */
@@ -112,6 +123,8 @@ export declare class HelixHeadlessImage extends HelixElement {
112
123
  * Which `src` the current status belongs to, so a new one starts over.
113
124
  */
114
125
  private answered;
126
+ /** The placeholder's wait for this `src`, and whether it is over. */
127
+ private readonly wait;
115
128
  /**
116
129
  * Whether this picture's answer has been given, or is on its way.
117
130
  *
@@ -177,6 +190,7 @@ export declare class HelixHeadlessImage extends HelixElement {
177
190
  private settle;
178
191
  protected willUpdate(changed: PropertyValues): void;
179
192
  protected updated(changed: PropertyValues): void;
193
+ private startWait;
180
194
  protected firstUpdated(changed: PropertyValues): void;
181
195
  /**
182
196
  * The picture's own state, under the platform's own names.
@@ -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, HelixElement } from '@fusion.dev/helix-core';
7
+ import { customElement, DelayController, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
@@ -13,12 +13,14 @@ let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
13
13
  this.crossOrigin = null;
14
14
  this.decoding = 'async';
15
15
  this.fetchPriority = 'auto';
16
+ this.placeholderDelay = 0;
16
17
  this.referrerPolicy = '';
17
18
  this.sizes = '';
18
19
  this.src = '';
19
20
  this.srcset = '';
20
21
  this.status = 'loading';
21
22
  this.answered = '';
23
+ this.wait = new DelayController(this);
22
24
  this.reported = false;
23
25
  this.synthesized = undefined;
24
26
  this.handleLoad = (event) => {
@@ -78,6 +80,9 @@ let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
78
80
  if (changed.has('src')) {
79
81
  this.settle();
80
82
  }
83
+ if (changed.has('src') || changed.has('placeholderDelay')) {
84
+ this.startWait();
85
+ }
81
86
  if (changed.has('status')) {
82
87
  if (this.status === 'loading') {
83
88
  this.removeAttribute('data-status');
@@ -85,8 +90,13 @@ let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
85
90
  else {
86
91
  this.setAttribute('data-status', this.status);
87
92
  }
93
+ this.toggleState('loaded', this.status === 'loaded');
94
+ this.toggleState('error', this.status === 'error');
88
95
  }
89
96
  }
97
+ startWait() {
98
+ this.wait.start(this.placeholderDelay > 0 ? this.placeholderDelay : undefined);
99
+ }
90
100
  firstUpdated(changed) {
91
101
  super.firstUpdated?.(changed);
92
102
  this.settle();
@@ -124,7 +134,7 @@ let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
124
134
  <span
125
135
  part="placeholder"
126
136
  aria-hidden="true"
127
- ?hidden=${this.status !== 'loading'}
137
+ ?hidden=${this.status !== 'loading' || (this.placeholderDelay > 0 && !this.wait.elapsed)}
128
138
  ><slot name="placeholder"></slot
129
139
  ></span>
130
140
  <span
@@ -150,6 +160,9 @@ __decorate([
150
160
  __decorate([
151
161
  property({ reflect: true })
152
162
  ], HelixHeadlessImage.prototype, "loading", void 0);
163
+ __decorate([
164
+ property({ attribute: 'placeholder-delay', type: Number })
165
+ ], HelixHeadlessImage.prototype, "placeholderDelay", void 0);
153
166
  __decorate([
154
167
  property({ attribute: 'referrerpolicy' })
155
168
  ], HelixHeadlessImage.prototype, "referrerPolicy", void 0);
@@ -36,6 +36,7 @@ declare const HelixHeadlessInput_base: (new (...args: any[]) => import("@fusion.
36
36
  *
37
37
  * @event {Event} change - Sent when the user finishes changing the value. Relayed from the native input.
38
38
  * @event {HelixBeforeChangeEvent} helix-before-change - The text is about to change. Cancelable: cancel it to keep the current text.
39
+ * @event {Event} helix-clear - Fired after the clear button has emptied the field, after `input` and `change`.
39
40
  * @event {Event} input - Fired as the value changes.
40
41
  *
41
42
  * @csspart clear - The button that empties the field.
@@ -235,6 +236,16 @@ export declare class HelixHeadlessInput extends HelixHeadlessInput_base {
235
236
  focus(options?: FocusOptions): void;
236
237
  /** Selects everything in the field. */
237
238
  select(): void;
239
+ /**
240
+ * Moves a number, a date or a time up by `step`, `count` times, as the native
241
+ * method does. It sends no event, as the native method sends none, and it
242
+ * throws `InvalidStateError` on a type with no step, such as `text`.
243
+ */
244
+ stepUp(count?: number): void;
245
+ /** Moves a number, a date or a time down by `step`, `count` times. See `stepUp`. */
246
+ stepDown(count?: number): void;
247
+ /** Takes the native control's answer, which knows `min`, `max` and `step`. */
248
+ private takeStep;
238
249
  /**
239
250
  * The caret, held across a value the consumer wrote rather than the reader.
240
251
  *
@@ -38,6 +38,7 @@ let HelixHeadlessInput = class HelixHeadlessInput extends MixinControllerValue(M
38
38
  this.handleClear = () => {
39
39
  if (this.value !== '' && this.requestValue('')) {
40
40
  this.announceChange();
41
+ this.dispatchEvent(new Event('helix-clear', { bubbles: true, composed: true }));
41
42
  }
42
43
  const control = this.control;
43
44
  if (control) {
@@ -119,6 +120,20 @@ let HelixHeadlessInput = class HelixHeadlessInput extends MixinControllerValue(M
119
120
  select() {
120
121
  this.control?.select();
121
122
  }
123
+ stepUp(count = 1) {
124
+ this.control?.stepUp(count);
125
+ this.takeStep();
126
+ }
127
+ stepDown(count = 1) {
128
+ this.control?.stepDown(count);
129
+ this.takeStep();
130
+ }
131
+ takeStep() {
132
+ const control = this.control;
133
+ if (control) {
134
+ this.value = control.value;
135
+ }
136
+ }
122
137
  willUpdate(changed) {
123
138
  super.willUpdate(changed);
124
139
  if (!this.hasUpdated) {
@@ -17,6 +17,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
17
17
  * The element binds the shortcut only when `within` is set. By default it draws
18
18
  * the key and listens for nothing.
19
19
  *
20
+ * **`for` names the control the shortcut works**, and the key writes its chord
21
+ * onto that control's `aria-keyshortcuts`, in the platform's words (`Meta+K`
22
+ * on an Apple keyboard, `Control+K` elsewhere), so a screen reader announces
23
+ * the shortcut with the control. A chord the page wrote there itself is left
24
+ * alone, and the key takes back only what it added.
25
+ *
20
26
  * @since 0.1.0
21
27
  * @status stable
22
28
  * @category typography
@@ -30,6 +36,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
30
36
  * @csspart kbd - The key.
31
37
  */
32
38
  export declare class HelixHeadlessKbd extends HelixElement {
39
+ /**
40
+ * The id of the control the shortcut works, which is given the chord as its
41
+ * `aria-keyshortcuts`.
42
+ *
43
+ * Names an element in the same tree, the way `Tooltip.for` does. Separate
44
+ * from `within`, which says where the key listens: a key can describe a
45
+ * shortcut the page binds itself.
46
+ */
47
+ for: string;
33
48
  /**
34
49
  * The key or chord to show, such as `mod+k`, `?`, or `/`.
35
50
  *
@@ -57,8 +72,13 @@ export declare class HelixHeadlessKbd extends HelixElement {
57
72
  within?: 'control' | 'document';
58
73
  /** The element the listener is on, kept so it can be taken off the same one. */
59
74
  private listening;
75
+ /** The control and the chord written onto it, so the same pair is taken back. */
76
+ private described;
60
77
  connectedCallback(): void;
61
78
  disconnectedCallback(): void;
79
+ /** Writes the chord onto the control `for` names, taking back one written before. */
80
+ private describe;
81
+ private undescribe;
62
82
  protected updated(changed: Map<string, unknown>): void;
63
83
  /**
64
84
  * The chord as this reader's keyboard labels it.
@@ -71,19 +91,6 @@ export declare class HelixHeadlessKbd extends HelixElement {
71
91
  private listen;
72
92
  private deafen;
73
93
  private readonly handleKeydown;
74
- /**
75
- * Whether this chord types a character, and so is indistinguishable from
76
- * typing.
77
- *
78
- * One code point means a character rather than a name: `/` and `?` are
79
- * printable, `escape` and `enter` are not. **Shift is allowed beside it**,
80
- * because Shift is how a character is typed: `shift+?` and `shift+a` are a
81
- * question mark and a capital A in a field, and taking them ate every one
82
- * a reader typed on the page.
83
- */
84
- private get printable();
85
- /** Whether a press is this chord, modifiers and all. */
86
- private matches_;
87
94
  protected firstUpdated(changed: PropertyValues): void;
88
95
  protected render(): TemplateResult;
89
96
  }
@@ -4,9 +4,10 @@ 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, HelixShortcutEvent } from '@fusion.dev/helix-core';
7
+ import { ariaChord, chordParts, customElement, elementById, HelixElement, HelixShortcutEvent, matchesChord, onApple, printableChord, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { claimShortcut, releaseShortcut } from '../shortcut.js';
10
11
  const typing = (node) => {
11
12
  if (node instanceof HTMLTextAreaElement) {
12
13
  return true;
@@ -66,20 +67,22 @@ const ELSEWHERE = {
66
67
  tab: 'Tab',
67
68
  up: '↑',
68
69
  };
69
- const onApple = () => typeof navigator !== 'undefined' && /mac|iphone|ipad|ipod/i.test(navigator.userAgent);
70
70
  let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
71
71
  constructor() {
72
72
  super(...arguments);
73
+ this.for = '';
73
74
  this.keys = '';
74
75
  this.handleKeydown = (event) => {
75
76
  const press = event;
76
77
  if (press.isComposing) {
77
78
  return;
78
79
  }
79
- if (this.within === 'document' && this.printable && typing(press.composedPath()[0])) {
80
+ if (this.within === 'document' &&
81
+ printableChord(this.keys) &&
82
+ typing(press.composedPath()[0])) {
80
83
  return;
81
84
  }
82
- if (!this.matches_(press)) {
85
+ if (!matchesChord(press, this.keys, onApple())) {
83
86
  return;
84
87
  }
85
88
  const shortcut = new HelixShortcutEvent({ keys: this.keys });
@@ -92,10 +95,30 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
92
95
  connectedCallback() {
93
96
  super.connectedCallback();
94
97
  this.listen();
98
+ requestAnimationFrame(() => this.describe());
95
99
  }
96
100
  disconnectedCallback() {
97
101
  super.disconnectedCallback();
98
102
  this.deafen();
103
+ this.undescribe();
104
+ }
105
+ describe() {
106
+ const control = this.isConnected && this.for !== '' ? elementById(this, this.for) : null;
107
+ const chord = ariaChord(this.keys, onApple());
108
+ if (control === this.described?.control && chord === this.described?.chord) {
109
+ return;
110
+ }
111
+ this.undescribe();
112
+ if (control && chord !== '') {
113
+ claimShortcut(control, chord);
114
+ this.described = { chord, control };
115
+ }
116
+ }
117
+ undescribe() {
118
+ if (this.described) {
119
+ releaseShortcut(this.described.control, this.described.chord);
120
+ this.described = undefined;
121
+ }
99
122
  }
100
123
  updated(changed) {
101
124
  super.updated(changed);
@@ -103,15 +126,15 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
103
126
  this.deafen();
104
127
  this.listen();
105
128
  }
129
+ if ((changed.has('for') && changed.get('for') !== undefined) ||
130
+ (changed.has('keys') && changed.get('keys') !== undefined)) {
131
+ this.describe();
132
+ }
106
133
  }
107
134
  get label() {
108
135
  const apple = !this.hydrating && onApple();
109
136
  const names = apple ? APPLE : ELSEWHERE;
110
- const parts = this.keys
111
- .split('+')
112
- .map((part) => part.trim())
113
- .filter((part) => part !== '')
114
- .map((part) => names[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part));
137
+ const parts = chordParts(this.keys).map((part) => names[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part));
115
138
  return parts.join(apple ? '' : '+');
116
139
  }
117
140
  listen() {
@@ -129,34 +152,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
129
152
  this.listening?.removeEventListener('keydown', this.handleKeydown);
130
153
  this.listening = undefined;
131
154
  }
132
- get printable() {
133
- const parts = this.keys
134
- .split('+')
135
- .map((part) => part.trim().toLowerCase())
136
- .filter((part) => part !== '');
137
- const key = parts.at(-1) ?? '';
138
- return [...key].length === 1 && parts.slice(0, -1).every((modifier) => modifier === 'shift');
139
- }
140
- matches_(press) {
141
- const parts = this.keys
142
- .split('+')
143
- .map((part) => part.trim().toLowerCase())
144
- .filter((part) => part !== '');
145
- if (parts.length === 0) {
146
- return false;
147
- }
148
- const wanted = parts[parts.length - 1] ?? '';
149
- const modifiers = new Set(parts.slice(0, -1));
150
- const apple = onApple();
151
- const ctrl = modifiers.has('ctrl') || (modifiers.has('mod') && !apple);
152
- const meta = modifiers.has('cmd') || modifiers.has('meta') || (modifiers.has('mod') && apple);
153
- const symbol = wanted.toLowerCase() === wanted.toUpperCase() && [...wanted].length === 1;
154
- return (press.key.toLowerCase() === wanted &&
155
- (modifiers.has('shift') ? press.shiftKey : symbol || !press.shiftKey) &&
156
- (modifiers.has('alt') || modifiers.has('opt') ? press.altKey : !press.altKey) &&
157
- (ctrl ? press.ctrlKey : !press.ctrlKey) &&
158
- (meta ? press.metaKey : !press.metaKey));
159
- }
160
155
  firstUpdated(changed) {
161
156
  super.firstUpdated(changed);
162
157
  this.finishHydration();
@@ -165,6 +160,9 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
165
160
  return html `<kbd part="kbd">${this.keys === '' ? html `<slot></slot>` : this.label}</kbd>`;
166
161
  }
167
162
  };
163
+ __decorate([
164
+ property()
165
+ ], HelixHeadlessKbd.prototype, "for", void 0);
168
166
  __decorate([
169
167
  property()
170
168
  ], HelixHeadlessKbd.prototype, "keys", void 0);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The bookkeeping that writes a chord onto the control a shortcut belongs to,
3
+ * in the words core's `ariaChord` spells it in.
4
+ *
5
+ * Beside the family's `index.ts`, because the attribute is a fact about
6
+ * another element, and not the key's template.
7
+ */
8
+ /** The space-separated list with `chord` added once. */
9
+ export declare const withShortcut: (list: string | null, chord: string) => string;
10
+ /** The list with `chord` taken out, or `null` when nothing is left. */
11
+ export declare const withoutShortcut: (list: string | null, chord: string) => string | null;
12
+ /**
13
+ * Adds a chord to the control's `aria-keyshortcuts`, leaving any the page
14
+ * wrote itself.
15
+ */
16
+ export declare const claimShortcut: (control: Element, chord: string) => void;
17
+ /** Takes back a chord this file wrote, and the attribute with the last one. */
18
+ export declare const releaseShortcut: (control: Element, chord: string) => void;
@@ -0,0 +1,35 @@
1
+ export const withShortcut = (list, chord) => {
2
+ const chords = (list ?? '').split(/\s+/).filter(Boolean);
3
+ return chords.includes(chord) ? chords.join(' ') : [...chords, chord].join(' ');
4
+ };
5
+ export const withoutShortcut = (list, chord) => {
6
+ const chords = (list ?? '').split(/\s+/).filter((one) => one !== '' && one !== chord);
7
+ return chords.length > 0 ? chords.join(' ') : null;
8
+ };
9
+ const WRITTEN = new WeakMap();
10
+ export const claimShortcut = (control, chord) => {
11
+ if (chord === '') {
12
+ return;
13
+ }
14
+ const before = control.getAttribute('aria-keyshortcuts');
15
+ if ((before ?? '').split(/\s+/).includes(chord) && !WRITTEN.get(control)?.has(chord)) {
16
+ return;
17
+ }
18
+ const written = WRITTEN.get(control) ?? new Set();
19
+ written.add(chord);
20
+ WRITTEN.set(control, written);
21
+ control.setAttribute('aria-keyshortcuts', withShortcut(before, chord));
22
+ };
23
+ export const releaseShortcut = (control, chord) => {
24
+ const written = WRITTEN.get(control);
25
+ if (!written?.delete(chord)) {
26
+ return;
27
+ }
28
+ const after = withoutShortcut(control.getAttribute('aria-keyshortcuts'), chord);
29
+ if (after === null) {
30
+ control.removeAttribute('aria-keyshortcuts');
31
+ }
32
+ else {
33
+ control.setAttribute('aria-keyshortcuts', after);
34
+ }
35
+ };
@@ -0,0 +1,8 @@
1
+ /** Whether a name is in the list, matched without regard to case, as a heading is. */
2
+ export declare const isHidden: (list: string, name: string) => boolean;
3
+ /**
4
+ * The list a press on this name leaves: without it where it was hidden, and
5
+ * with it at the end where it was shown. Written back with a comma and a
6
+ * space, as a person writes a list.
7
+ */
8
+ export declare const toggledNames: (list: string, name: string) => string;
@@ -0,0 +1,13 @@
1
+ import { commaList, seriesKey } from '@fusion.dev/helix-core';
2
+ export const isHidden = (list, name) => {
3
+ const key = seriesKey(name);
4
+ return commaList(list).some((one) => seriesKey(one) === key);
5
+ };
6
+ export const toggledNames = (list, name) => {
7
+ const key = seriesKey(name);
8
+ const now = commaList(list);
9
+ const next = isHidden(list, name)
10
+ ? now.filter((one) => seriesKey(one) !== key)
11
+ : [...now, name.trim()];
12
+ return next.join(', ');
13
+ };
@@ -18,17 +18,28 @@ import { type PropertyValues, type TemplateResult } from 'lit';
18
18
  *
19
19
  * The table is resolved on the client. There is no server pass.
20
20
  *
21
+ * **`toggles` makes each entry a button that hides its series.** Every chart
22
+ * reading the same table draws without a hidden series and fits its scale to
23
+ * the rest, and the series keeps its color. The button says whether its
24
+ * series is shown with `aria-pressed`. `hidden-series` holds the hidden names,
25
+ * so a page can start with some hidden or read which are.
26
+ *
21
27
  * @since 0.1.0
22
28
  * @status stable
23
29
  * @category charts
24
30
  * @aliases Key, Series Key
25
31
  * @documentation https://fusion.dev/helix/components/legend
26
32
  *
33
+ * @event {Event} change - Fired once a toggle has hidden or shown a series.
34
+ * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a toggle hides or shows a series, with the `hidden-series` it would leave. Cancelable.
35
+ *
27
36
  * @csspart entry - One name and its swatch. Draw the swatch with
28
37
  * `::part(entry)::before` in your stylesheet. The entry holds its series color
29
38
  * as `--chart-series`, and this tier ships no CSS.
39
+ * @csspart entry-hidden - An entry whose series is hidden. Set alongside `entry`.
30
40
  * @csspart list - The list of entries.
31
41
  * @csspart name - One series name.
42
+ * @csspart toggle - The button around a name under `toggles`, which hides or shows its series.
32
43
  */
33
44
  export declare class HelixHeadlessLegend extends HelixElement {
34
45
  /**
@@ -62,6 +73,15 @@ export declare class HelixHeadlessLegend extends HelixElement {
62
73
  * `Tooltip.for` does, so it does not reflect and nothing styles on it.
63
74
  */
64
75
  for: string;
76
+ /**
77
+ * The series hidden on the table, by name, separated by commas: what
78
+ * `toggles` hides and shows.
79
+ *
80
+ * Set it to start with series hidden. Every chart reading the same table
81
+ * draws without them. Commas only, because a column's heading may hold a
82
+ * space. For a ring the names are the rows.
83
+ */
84
+ hiddenSeries: string;
65
85
  /**
66
86
  * The name of the list, which a screen reader announces before its entries.
67
87
  *
@@ -69,8 +89,37 @@ export declare class HelixHeadlessLegend extends HelixElement {
69
89
  * charts has a legend list for each that says which chart it keys.
70
90
  */
71
91
  label: string;
92
+ /**
93
+ * Makes each entry a button that hides or shows its series in every chart
94
+ * reading the same table.
95
+ *
96
+ * Absent, the legend only displays, which is what it did before this
97
+ * existed. Reflected, because the styled tier draws a button differently
98
+ * from a plain entry.
99
+ */
100
+ toggles: boolean;
72
101
  protected readonly source: SourceController;
102
+ /** Whether this legend has written the table's hidden series, so leaving undoes it. */
103
+ private wrote;
73
104
  protected willUpdate(changed: PropertyValues<this>): void;
105
+ /**
106
+ * Hands `hidden-series` to the table, where every chart reading it finds
107
+ * it.
108
+ *
109
+ * After the update rather than before, because a server has no table and a
110
+ * hydrating render must draw what the server drew: the charts follow a frame
111
+ * after the legend, which is when they draw at all.
112
+ */
113
+ protected updated(changed: PropertyValues<this>): void;
114
+ disconnectedCallback(): void;
115
+ /**
116
+ * Hides the series of this name, or shows it again, asking first.
117
+ *
118
+ * `helix-before-change` carries the `hidden-series` the press would leave,
119
+ * and a listener that refuses it keeps the chart as it is. `change` follows
120
+ * once the series is hidden or shown.
121
+ */
122
+ protected toggle(name: string): void;
74
123
  /**
75
124
  * The names, in the order the chart will draw them.
76
125
  *
@@ -82,6 +131,13 @@ export declare class HelixHeadlessLegend extends HelixElement {
82
131
  */
83
132
  protected names(): string[];
84
133
  protected render(): TemplateResult;
134
+ /**
135
+ * The button around a name under `toggles`, behind a seam a tier can move.
136
+ *
137
+ * Pressed while its series is shown, which is the state a reader changes by
138
+ * pressing it: a toggle button says whether its thing is on.
139
+ */
140
+ protected renderToggle(name: string, off: boolean): TemplateResult;
85
141
  }
86
142
  declare global {
87
143
  interface HTMLElementTagNameMap {