@fusion.dev/helix-headless 0.142.0 → 0.143.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 (217) hide show
  1. package/custom-elements.json +16710 -707
  2. package/dist/accordion/item/accordion-item.d.ts +38 -2
  3. package/dist/accordion/item/accordion-item.js +38 -6
  4. package/dist/accordion/root/accordion-root.d.ts +23 -1
  5. package/dist/accordion/root/accordion-root.js +39 -3
  6. package/dist/announcement/root/announcement-root.d.ts +11 -7
  7. package/dist/announcement/root/announcement-root.js +1 -1
  8. package/dist/app-header/root/app-header-root.d.ts +21 -0
  9. package/dist/app-header/root/app-header-root.js +31 -5
  10. package/dist/area-chart/root/area-chart-root.js +16 -5
  11. package/dist/avatar/group/avatar-group.d.ts +24 -3
  12. package/dist/avatar/group/avatar-group.js +34 -2
  13. package/dist/avatar/root/avatar-root.d.ts +3 -1
  14. package/dist/avatar/root/avatar-root.js +8 -1
  15. package/dist/badge/root/badge-root.d.ts +8 -0
  16. package/dist/bar-chart/root/bar-chart-root.d.ts +19 -3
  17. package/dist/bar-chart/root/bar-chart-root.js +43 -13
  18. package/dist/blockquote/root/blockquote-root.d.ts +12 -7
  19. package/dist/blockquote/root/blockquote-root.js +6 -4
  20. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +9 -3
  21. package/dist/breadcrumbs/root/breadcrumbs-root.js +9 -1
  22. package/dist/button/root/button-root.d.ts +37 -0
  23. package/dist/button/root/button-root.js +44 -0
  24. package/dist/callout/root/callout-root.d.ts +14 -5
  25. package/dist/callout/root/callout-root.js +11 -3
  26. package/dist/canvas/root/canvas-root.d.ts +1 -0
  27. package/dist/canvas/root/canvas-root.js +10 -1
  28. package/dist/carousel/root/carousel-root.d.ts +47 -5
  29. package/dist/carousel/root/carousel-root.js +53 -21
  30. package/dist/chat/composer/chat-composer.d.ts +13 -11
  31. package/dist/chat/composer/chat-composer.js +6 -12
  32. package/dist/chat/message/chat-message.js +8 -1
  33. package/dist/chat/root/chat-root.d.ts +1 -0
  34. package/dist/chat/root/chat-root.js +2 -1
  35. package/dist/checkbox/root/checkbox-root.d.ts +7 -0
  36. package/dist/checkbox/root/checkbox-root.js +7 -3
  37. package/dist/checkbox-group/root/checkbox-group-root.d.ts +65 -32
  38. package/dist/checkbox-group/root/checkbox-group-root.js +85 -37
  39. package/dist/code/root/code-root.d.ts +1 -1
  40. package/dist/code-block/root/code-block-root.d.ts +19 -0
  41. package/dist/code-block/root/code-block-root.js +21 -1
  42. package/dist/color-input/root/color-input-root.d.ts +26 -1
  43. package/dist/color-input/root/color-input-root.js +32 -3
  44. package/dist/color-picker/root/color-picker-root.d.ts +37 -7
  45. package/dist/color-picker/root/color-picker-root.js +83 -6
  46. package/dist/combobox/root/combobox-root.d.ts +90 -2
  47. package/dist/combobox/root/combobox-root.js +160 -19
  48. package/dist/command/root/command-root.d.ts +17 -6
  49. package/dist/command/root/command-root.js +12 -1
  50. package/dist/copy-button/root/copy-button-root.d.ts +21 -6
  51. package/dist/copy-button/root/copy-button-root.js +11 -1
  52. package/dist/data-table/root/data-table-root.d.ts +164 -10
  53. package/dist/data-table/root/data-table-root.js +488 -57
  54. package/dist/date-input/root/date-input-root.d.ts +53 -8
  55. package/dist/date-input/root/date-input-root.js +70 -18
  56. package/dist/date-picker/root/date-picker-root.d.ts +84 -17
  57. package/dist/date-picker/root/date-picker-root.js +125 -20
  58. package/dist/dialog/root/dialog-root.d.ts +38 -10
  59. package/dist/dialog/root/dialog-root.js +55 -2
  60. package/dist/donut-chart/root/donut-chart-root.d.ts +39 -15
  61. package/dist/donut-chart/root/donut-chart-root.js +30 -10
  62. package/dist/drawer/root/drawer-root.d.ts +32 -7
  63. package/dist/drawer/root/drawer-root.js +50 -5
  64. package/dist/drilldown/item/drilldown-item.d.ts +16 -13
  65. package/dist/drilldown/item/drilldown-item.js +6 -2
  66. package/dist/drilldown/root/drilldown-root.d.ts +21 -5
  67. package/dist/drilldown/root/drilldown-root.js +26 -10
  68. package/dist/element-events.generated.js +35 -9
  69. package/dist/file-input/root/file-input-root.d.ts +18 -1
  70. package/dist/file-input/root/file-input-root.js +34 -8
  71. package/dist/filter-input/menu/filter-menu.d.ts +9 -0
  72. package/dist/filter-input/menu/filter-menu.js +8 -0
  73. package/dist/filter-input/root/filter-input-root.d.ts +5 -2
  74. package/dist/filter-input/root/filter-input-root.js +22 -7
  75. package/dist/format-date/root/format-date-root.d.ts +11 -3
  76. package/dist/format-date/root/format-date-root.js +26 -5
  77. package/dist/graph/root/graph-root.d.ts +24 -3
  78. package/dist/graph/root/graph-root.js +66 -12
  79. package/dist/heatmap/root/buckets.d.ts +2 -1
  80. package/dist/heatmap/root/buckets.js +5 -1
  81. package/dist/heatmap/root/heatmap-root.d.ts +27 -6
  82. package/dist/heatmap/root/heatmap-root.js +23 -6
  83. package/dist/icon/root/icon-root.d.ts +24 -3
  84. package/dist/icon/root/icon-root.js +34 -1
  85. package/dist/icon-button/root/icon-button-root.d.ts +1 -1
  86. package/dist/image/root/image-root.d.ts +9 -2
  87. package/dist/image/root/image-root.js +8 -2
  88. package/dist/input/root/input-root.d.ts +51 -9
  89. package/dist/input/root/input-root.js +46 -5
  90. package/dist/kbd/root/kbd-root.d.ts +12 -4
  91. package/dist/kbd/root/kbd-root.js +10 -11
  92. package/dist/legend/root/legend-root.d.ts +7 -0
  93. package/dist/legend/root/legend-root.js +7 -2
  94. package/dist/lightbox/item/lightbox-item.d.ts +2 -1
  95. package/dist/lightbox/root/lightbox-root.d.ts +16 -5
  96. package/dist/lightbox/root/lightbox-root.js +24 -3
  97. package/dist/line-chart/root/line-chart-root.d.ts +19 -2
  98. package/dist/line-chart/root/line-chart-root.js +40 -13
  99. package/dist/list/item/list-item.d.ts +11 -5
  100. package/dist/list/item/list-item.js +5 -6
  101. package/dist/listbox/root/listbox-root.d.ts +111 -2
  102. package/dist/listbox/root/listbox-root.js +144 -16
  103. package/dist/map/root/map-root.d.ts +19 -4
  104. package/dist/map/root/map-root.js +36 -7
  105. package/dist/megamenu/root/megamenu-root.d.ts +10 -1
  106. package/dist/megamenu/root/megamenu-root.js +10 -1
  107. package/dist/menu/item/menu-item.d.ts +41 -12
  108. package/dist/menu/item/menu-item.js +46 -3
  109. package/dist/menu/root/menu-root.d.ts +32 -2
  110. package/dist/menu/root/menu-root.js +48 -8
  111. package/dist/meter/root/meter-root.d.ts +6 -0
  112. package/dist/meter/root/meter-root.js +18 -2
  113. package/dist/navigation/root/navigation-root.d.ts +10 -2
  114. package/dist/navigation/root/navigation-root.js +13 -2
  115. package/dist/notifications/group/notification-group.d.ts +1 -0
  116. package/dist/notifications/notification/notification.d.ts +12 -0
  117. package/dist/notifications/notification/notification.js +31 -2
  118. package/dist/notifications/root/notifications-root.d.ts +7 -0
  119. package/dist/notifications/root/notifications-root.js +17 -2
  120. package/dist/number-input/root/number-input-root.d.ts +62 -7
  121. package/dist/number-input/root/number-input-root.js +68 -16
  122. package/dist/object-inspector/root/object-inspector-root.d.ts +48 -5
  123. package/dist/object-inspector/root/object-inspector-root.js +91 -11
  124. package/dist/option/root/option-root.d.ts +4 -1
  125. package/dist/option/root/option-root.js +1 -0
  126. package/dist/outline/root/outline-root.d.ts +13 -2
  127. package/dist/outline/root/outline-root.js +4 -2
  128. package/dist/pagination/root/pagination-root.d.ts +19 -1
  129. package/dist/pagination/root/pagination-root.js +49 -9
  130. package/dist/pagination/size/pagination-size.d.ts +13 -3
  131. package/dist/pagination/size/pagination-size.js +7 -0
  132. package/dist/pin-input/root/pin-input-root.d.ts +16 -0
  133. package/dist/pin-input/root/pin-input-root.js +19 -5
  134. package/dist/popover/root/popover-root.d.ts +19 -2
  135. package/dist/popover/root/popover-root.js +22 -1
  136. package/dist/progress/root/progress-root.d.ts +16 -1
  137. package/dist/progress/root/progress-root.js +31 -7
  138. package/dist/qr-code/root/qr-code-root.d.ts +19 -0
  139. package/dist/qr-code/root/qr-code-root.js +29 -1
  140. package/dist/radio/group/radio-group.d.ts +41 -0
  141. package/dist/radio/group/radio-group.js +50 -14
  142. package/dist/radio/root/radio-root.d.ts +8 -5
  143. package/dist/radio/root/radio-root.js +1 -2
  144. package/dist/rating/root/rating-root.d.ts +24 -1
  145. package/dist/rating/root/rating-root.js +24 -7
  146. package/dist/scatter-chart/root/scatter-chart-root.js +15 -7
  147. package/dist/select/root/select-root.d.ts +69 -3
  148. package/dist/select/root/select-root.js +92 -8
  149. package/dist/skeleton/root/skeleton-root.d.ts +14 -3
  150. package/dist/skeleton/root/skeleton-root.js +17 -3
  151. package/dist/skip-link/root/skip-link-root.d.ts +10 -0
  152. package/dist/skip-link/root/skip-link-root.js +15 -1
  153. package/dist/slider/root/slider-root.d.ts +20 -2
  154. package/dist/slider/root/slider-root.js +41 -3
  155. package/dist/sparkline/root/sparkline-root.d.ts +9 -10
  156. package/dist/speed-dial/item/speed-dial-item.d.ts +11 -5
  157. package/dist/speed-dial/item/speed-dial-item.js +7 -2
  158. package/dist/speed-dial/root/speed-dial-root.d.ts +15 -4
  159. package/dist/speed-dial/root/speed-dial-root.js +19 -5
  160. package/dist/splitter/root/splitter-root.d.ts +28 -6
  161. package/dist/splitter/root/splitter-root.js +35 -1
  162. package/dist/stat/root/stat-root.d.ts +7 -1
  163. package/dist/stat/root/stat-root.js +14 -1
  164. package/dist/steps/root/steps-root.js +1 -1
  165. package/dist/switch/root/switch-root.d.ts +2 -0
  166. package/dist/switch/root/switch-root.js +1 -0
  167. package/dist/table/cell/table-cell.d.ts +24 -4
  168. package/dist/table/cell/table-cell.js +21 -1
  169. package/dist/table/column/table-column.d.ts +32 -6
  170. package/dist/table/column/table-column.js +36 -6
  171. package/dist/table/root/table-root.d.ts +94 -3
  172. package/dist/table/root/table-root.js +128 -5
  173. package/dist/table/row/table-row.d.ts +7 -0
  174. package/dist/table/row/table-row.js +9 -1
  175. package/dist/tabs/root/tabs-root.d.ts +6 -1
  176. package/dist/tabs/root/tabs-root.js +6 -2
  177. package/dist/tag/root/tag-root.d.ts +23 -1
  178. package/dist/tag/root/tag-root.js +16 -2
  179. package/dist/textarea/root/textarea-root.d.ts +4 -0
  180. package/dist/textarea/root/textarea-root.js +1 -1
  181. package/dist/time-input/root/time-input-root.d.ts +28 -2
  182. package/dist/time-input/root/time-input-root.js +46 -10
  183. package/dist/time-picker/root/time-picker-root.d.ts +101 -18
  184. package/dist/time-picker/root/time-picker-root.js +115 -16
  185. package/dist/toast/root/toast-root.d.ts +64 -1
  186. package/dist/toast/root/toast-root.js +147 -2
  187. package/dist/toggle/group/toggle-group.d.ts +38 -1
  188. package/dist/toggle/group/toggle-group.js +34 -8
  189. package/dist/toggle/root/toggle-root.d.ts +14 -10
  190. package/dist/toggle/root/toggle-root.js +3 -3
  191. package/dist/toolbar/root/toolbar-root.d.ts +55 -15
  192. package/dist/toolbar/root/toolbar-root.js +99 -16
  193. package/dist/tooltip/root/tooltip-root.d.ts +37 -4
  194. package/dist/tooltip/root/tooltip-root.js +48 -0
  195. package/dist/tour/root/tour-root.d.ts +34 -5
  196. package/dist/tour/root/tour-root.js +51 -14
  197. package/dist/tour/stop/tour-stop.d.ts +18 -1
  198. package/dist/tour/stop/tour-stop.js +25 -5
  199. package/dist/tree/item/tree-item.d.ts +21 -5
  200. package/dist/tree/item/tree-item.js +14 -4
  201. package/dist/tree/root/tree-root.d.ts +29 -5
  202. package/dist/tree/root/tree-root.js +64 -8
  203. package/dist/trend/root/trend-root.d.ts +23 -1
  204. package/dist/trend/root/trend-root.js +28 -7
  205. package/dist/video/root/keys.d.ts +5 -0
  206. package/dist/video/root/keys.js +13 -1
  207. package/dist/video/root/video-root.d.ts +19 -0
  208. package/dist/video/root/video-root.js +48 -4
  209. package/dist/video-playlist/menu/video-playlist-menu.d.ts +20 -2
  210. package/dist/video-playlist/menu/video-playlist-menu.js +27 -5
  211. package/dist/video-playlist/root/video-playlist-root.js +6 -3
  212. package/dist/virtual-list/root/virtual-list-root.d.ts +52 -4
  213. package/dist/virtual-list/root/virtual-list-root.js +103 -21
  214. package/lint.json +1 -1
  215. package/package.json +5 -5
  216. package/types/jsx.d.ts +4348 -965
  217. package/types/vue.d.ts +972 -232
@@ -21,10 +21,12 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
21
21
  this.autocomplete = '';
22
22
  this.decrementLabel = 'Decrease';
23
23
  this.incrementLabel = 'Increase';
24
+ this.locale = '';
24
25
  this.placeholder = '';
25
26
  this.readOnly = false;
26
27
  this.scrub = false;
27
28
  this.step = 1;
29
+ this.valueText = '';
28
30
  this.scrubFrom = 0;
29
31
  this.scrubValue = 0;
30
32
  this.scrubStart = '';
@@ -75,16 +77,13 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
75
77
  restoreValue(control, this.value);
76
78
  };
77
79
  this.handleBlur = () => {
78
- const control = this.control;
79
- const parsed = this.numeric;
80
- if (!control || parsed === null) {
81
- return;
80
+ if (this.settle()) {
81
+ this.announceChange();
82
82
  }
83
- this.requestValue(String(this.clamp(parsed)));
84
- restoreValue(control, this.value);
85
83
  };
86
84
  this.handleChange = (event) => {
87
85
  event.stopPropagation();
86
+ this.settle();
88
87
  this.announceChange();
89
88
  };
90
89
  this.handleKeydown = (event) => {
@@ -117,21 +116,32 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
117
116
  }
118
117
  event.preventDefault();
119
118
  };
119
+ this.handleKeypress = (event) => {
120
+ if (event.key === 'Enter' && !event.defaultPrevented) {
121
+ this.submitImplicitly();
122
+ }
123
+ };
120
124
  }
121
- static { this.shadowRootOptions = {
122
- ...HelixFormElement.shadowRootOptions,
123
- delegatesFocus: true,
124
- }; }
125
125
  get control() {
126
126
  return this.partNamed('control');
127
127
  }
128
128
  get numeric() {
129
- if (this.value.trim() === '') {
129
+ const text = this.value.trim();
130
+ if (text === '') {
130
131
  return null;
131
132
  }
132
- const parsed = Number(this.value);
133
+ const read = decimalComma(this.locale) && text.includes(',') && !text.includes('.')
134
+ ? text.replace(',', '.')
135
+ : text;
136
+ const parsed = read.includes(',') ? Number.NaN : Number(read);
133
137
  return Number.isFinite(parsed) ? parsed : null;
134
138
  }
139
+ onStep(value) {
140
+ const step = Math.abs(this.step) || 1;
141
+ const anchor = this.min ?? 0;
142
+ const steps = (value - anchor) / step;
143
+ return Math.abs(steps - Math.round(steps)) < 1e-9;
144
+ }
135
145
  clamp(value) {
136
146
  const step = Math.abs(this.step) || 1;
137
147
  const anchor = this.min ?? 0;
@@ -168,13 +178,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
168
178
  this.nudge(large ? -(this.largeStep ?? 10) : -1);
169
179
  }
170
180
  renderLabel() {
171
- if (!this.scrub) {
181
+ if (!this.scrub || !this.labeled) {
172
182
  return super.renderLabel();
173
183
  }
174
184
  const hidden = this.label === '' ? this.getAttribute('aria-label') : null;
175
- if (this.label === '' && !hidden) {
176
- return nothing;
177
- }
178
185
  return html `<label
179
186
  part="label"
180
187
  for=${this.controlId}
@@ -186,6 +193,17 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
186
193
  ><slot name="label">${this.label || hidden}</slot></label
187
194
  >`;
188
195
  }
196
+ settle() {
197
+ const control = this.control;
198
+ const parsed = this.numeric;
199
+ if (!control || parsed === null) {
200
+ return false;
201
+ }
202
+ const before = this.value;
203
+ this.requestValue(String(this.clamp(parsed)));
204
+ restoreValue(control, this.value);
205
+ return this.value !== before;
206
+ }
189
207
  sync() {
190
208
  const control = this.control;
191
209
  this.internals.setFormValue(this.value);
@@ -212,6 +230,14 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
212
230
  max: this.max,
213
231
  }), control);
214
232
  }
233
+ else if (parsed !== null && !this.onStep(parsed)) {
234
+ const step = Math.abs(this.step) || 1;
235
+ const anchor = this.min ?? 0;
236
+ const below = this.clamp(anchor + Math.floor((parsed - anchor) / step) * step);
237
+ const above = this.clamp(anchor + Math.ceil((parsed - anchor) / step) * step);
238
+ this.internals.setValidity({ stepMismatch: true }, fillTemplate(this.stepMismatchMessage ||
239
+ 'Please enter a valid value. The two nearest valid values are {below} and {above}.', { above, below }), control);
240
+ }
215
241
  else {
216
242
  this.internals.setValidity({}, '', control);
217
243
  }
@@ -239,6 +265,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
239
265
  this.releaseCaret = undefined;
240
266
  this.sync();
241
267
  }
268
+ get blocksImplicitSubmission() {
269
+ return true;
270
+ }
242
271
  render() {
243
272
  const parsed = this.numeric;
244
273
  const atMin = parsed !== null && this.min !== undefined && parsed <= this.min;
@@ -274,6 +303,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
274
303
  aria-valuemin=${this.min ?? nothing}
275
304
  aria-valuemax=${this.max ?? nothing}
276
305
  aria-valuenow=${parsed ?? nothing}
306
+ aria-valuetext=${this.valueText && parsed !== null
307
+ ? fillTemplate(this.valueText, { value: this.value })
308
+ : nothing}
277
309
  ?disabled=${this.isDisabled}
278
310
  ?readonly=${this.readOnly}
279
311
  ?required=${this.required}
@@ -281,6 +313,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
281
313
  @change=${this.handleChange}
282
314
  @input=${this.handleInput}
283
315
  @keydown=${this.handleKeydown}
316
+ @keypress=${this.handleKeypress}
284
317
  />
285
318
  <slot name="end"></slot>
286
319
  <button
@@ -320,6 +353,9 @@ __decorate([
320
353
  __decorate([
321
354
  property({ attribute: 'large-step', type: Number })
322
355
  ], HelixHeadlessNumberInput.prototype, "largeStep", void 0);
356
+ __decorate([
357
+ property()
358
+ ], HelixHeadlessNumberInput.prototype, "locale", void 0);
323
359
  __decorate([
324
360
  property({ type: Number })
325
361
  ], HelixHeadlessNumberInput.prototype, "max", void 0);
@@ -344,10 +380,26 @@ __decorate([
344
380
  __decorate([
345
381
  property({ type: Number })
346
382
  ], HelixHeadlessNumberInput.prototype, "step", void 0);
383
+ __decorate([
384
+ property({ attribute: 'step-mismatch-message' })
385
+ ], HelixHeadlessNumberInput.prototype, "stepMismatchMessage", void 0);
386
+ __decorate([
387
+ property({ attribute: 'value-text' })
388
+ ], HelixHeadlessNumberInput.prototype, "valueText", void 0);
347
389
  HelixHeadlessNumberInput = __decorate([
348
390
  customElement('helix-headless-number-input')
349
391
  ], HelixHeadlessNumberInput);
350
392
  export { HelixHeadlessNumberInput };
393
+ const commas = new Map();
394
+ const decimalComma = (locale) => {
395
+ let comma = commas.get(locale);
396
+ if (comma === undefined) {
397
+ const parts = new Intl.NumberFormat(locale || undefined).formatToParts(1.5);
398
+ comma = parts.find((part) => part.type === 'decimal')?.value === ',';
399
+ commas.set(locale, comma);
400
+ }
401
+ return comma;
402
+ };
351
403
  const decimals = (value) => {
352
404
  const text = String(value);
353
405
  const exponent = /e-(\d+)$/.exec(text);
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../copy-button/index.js';
4
4
  /** One visible line of the tree: a value, where it sits, and whether it is open. */
@@ -28,9 +28,15 @@ interface Row {
28
28
  *
29
29
  * The tree is one tab stop. Up and Down move between rows, Right opens a branch
30
30
  * or moves into it, Left closes it or moves to its parent, and Home and End
31
- * move to the first and last rows. Focus stays on the tree, and
32
- * `aria-activedescendant` marks the active row. Each row carries its real
33
- * level, position, and set size, including in a windowed tree.
31
+ * move to the first and last rows. Typing moves to the next row whose key
32
+ * starts with what was typed, and `*` opens every branch beside the active
33
+ * row. Focus stays on the tree, and `aria-activedescendant` marks the active
34
+ * row. Each row carries its real level, position, and set size, including in
35
+ * a windowed tree.
36
+ *
37
+ * A new `value` keeps the branches a reader opened or closed, and the active
38
+ * row, where the new value still has them. A branch the old value did not have
39
+ * opens to `default-depth`.
34
40
  *
35
41
  * The copy button copies the active row, a branch as JSON and a string as its
36
42
  * text. So do Command+C and Control+C while the tree has focus and no text is
@@ -110,6 +116,14 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
110
116
  * page's markup should not take the page down over a comma.
111
117
  */
112
118
  for: string;
119
+ /** How an array of one entry counts it. `{count}` is replaced with the count. */
120
+ itemLabel: string;
121
+ /** How an array of any other number of entries counts them. `{count}` is replaced with the count. */
122
+ itemsLabel: string;
123
+ /** How an object of one key counts it. `{count}` is replaced with the count. */
124
+ keyLabel: string;
125
+ /** How an object of any other number of keys counts them. `{count}` is replaced with the count. */
126
+ keysLabel: string;
113
127
  /**
114
128
  * Accessible name for the tree, and the text of the header where there is one.
115
129
  */
@@ -128,6 +142,11 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
128
142
  * reach a value.
129
143
  */
130
144
  noCopy: boolean;
145
+ /**
146
+ * The tree's accessible name when `label` is empty and the host names it
147
+ * with neither `aria-label` nor `aria-labelledby`.
148
+ */
149
+ treeLabel: string;
131
150
  /**
132
151
  * The value to show, when your code already holds it as an object.
133
152
  *
@@ -152,6 +171,12 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
152
171
  private active;
153
172
  /** Set by a keyboard move, so the next render brings the active row into view. */
154
173
  private revealing;
174
+ /** What has been typed toward a key, and when the last letter came. */
175
+ private typed;
176
+ /** The branches of the last value seen, by path, so a new value can tell old ones from new. */
177
+ private branches;
178
+ /** Carries a host `aria-label` and `aria-labelledby` to the tree, which is what takes focus. */
179
+ protected readonly aria: AriaForwardController;
155
180
  /**
156
181
  * How tall a row is taken to be before it has been drawn, in pixels.
157
182
  *
@@ -169,7 +194,15 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
169
194
  private readonly copier;
170
195
  /** Every visible row, in order, for the tree as it stands. */
171
196
  private flatten;
172
- /** Opens every branch shallower than `defaultDepth`. */
197
+ /**
198
+ * Opens every branch shallower than `defaultDepth`.
199
+ *
200
+ * `keep` is for a new value: a branch the last value also had keeps whether
201
+ * a reader opened or closed it, and only a new one takes the default.
202
+ * **Assigning a value used to close everything**, so an inspector showing
203
+ * live data, a polled response or a debug panel, threw away the reader's
204
+ * place on every update.
205
+ */
173
206
  private seed;
174
207
  protected willUpdate(changed: PropertyValues): void;
175
208
  protected firstUpdated(changed: PropertyValues): void;
@@ -193,6 +226,16 @@ export declare class HelixHeadlessObjectInspector extends HelixElement {
193
226
  private toggle;
194
227
  private move;
195
228
  private readonly handleKeydown;
229
+ /** Opens every branch at the active row's level under the same parent, as `*` asks. */
230
+ private openSiblings;
231
+ /**
232
+ * Moves to the next row whose key starts with what was typed.
233
+ *
234
+ * Letters typed within half a second of each other run together, so `me`
235
+ * finds `meta` past `message`. A repeated single letter cycles through the
236
+ * keys that start with it.
237
+ */
238
+ private typeahead;
196
239
  private handleRowClick;
197
240
  private renderValue;
198
241
  /**
@@ -5,7 +5,7 @@ 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 HelixHeadlessObjectInspector_1;
8
- import { CopyController, customElement, elementById, fillTemplate, HelixElement, inlineStep, scrollItemIntoView, searchableById, VirtualizeController, } from '@fusion.dev/helix-core';
8
+ import { AriaForwardController, CopyController, customElement, elementById, fillTemplate, formatNumbers, HelixElement, inlineStep, scrollItemIntoView, searchableById, VirtualizeController, } from '@fusion.dev/helix-core';
9
9
  import { caretDown, caretRight } from '@fusion.dev/helix-icons';
10
10
  import { html, nothing } from 'lit';
11
11
  import { property, state } from 'lit/decorators.js';
@@ -22,14 +22,25 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
22
22
  this.copyValueLabel = 'Copy value';
23
23
  this.defaultDepth = 1;
24
24
  this.for = '';
25
+ this.itemLabel = '{count} item';
26
+ this.itemsLabel = '{count} items';
27
+ this.keyLabel = '{count} key';
28
+ this.keysLabel = '{count} keys';
25
29
  this.label = '';
26
30
  this.noCopy = false;
31
+ this.treeLabel = 'Value';
27
32
  this.value = undefined;
28
33
  this.windowed = false;
29
34
  this.read = undefined;
30
35
  this.opened = new Set();
31
36
  this.active = 0;
32
37
  this.revealing = false;
38
+ this.typed = { at: 0, text: '' };
39
+ this.branches = new Set();
40
+ this.aria = new AriaForwardController(this, {
41
+ label: true,
42
+ target: () => this.partNamed('tree'),
43
+ });
33
44
  this.virtualize = new VirtualizeController(this, {
34
45
  container: () => (this.windowed ? this.partNamed('tree') : null),
35
46
  count: () => this.rows.length,
@@ -92,8 +103,14 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
92
103
  this.toggle(row);
93
104
  }
94
105
  break;
106
+ case '*':
107
+ this.openSiblings(row);
108
+ break;
95
109
  default:
96
- return;
110
+ if (event.key.length !== 1 || event.altKey || event.ctrlKey || event.metaKey) {
111
+ return;
112
+ }
113
+ this.typeahead(event.key);
97
114
  }
98
115
  event.preventDefault();
99
116
  };
@@ -131,20 +148,28 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
131
148
  visit(this.root, '', 0, 1, 1);
132
149
  return rows;
133
150
  }
134
- seed() {
151
+ seed(keep) {
135
152
  const opened = new Set();
153
+ const branches = new Set();
136
154
  const visit = (value, path, depth) => {
137
- if (!branchy(value) || depth >= this.defaultDepth) {
155
+ if (!branchy(value)) {
138
156
  return;
139
157
  }
140
- opened.add(path);
158
+ branches.add(path);
159
+ const known = keep && this.branches.has(path);
160
+ if (known ? this.opened.has(path) : depth < this.defaultDepth) {
161
+ opened.add(path);
162
+ }
141
163
  for (const [name, item] of entries(value)) {
142
164
  visit(item, child(path, name), depth + 1);
143
165
  }
144
166
  };
145
167
  visit(this.root, '', 0);
168
+ this.branches = branches;
146
169
  this.opened = opened;
147
- this.active = 0;
170
+ if (!keep) {
171
+ this.active = 0;
172
+ }
148
173
  }
149
174
  willUpdate(changed) {
150
175
  super.willUpdate(changed);
@@ -152,15 +177,17 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
152
177
  this.loadedFor = this.for;
153
178
  this.load();
154
179
  }
180
+ const was = this.rows[this.active]?.path;
155
181
  if (changed.has('value') || changed.has('read') || changed.has('defaultDepth')) {
156
- this.seed();
182
+ this.seed(!changed.has('defaultDepth'));
157
183
  }
158
184
  if (changed.has('value') ||
159
185
  changed.has('read') ||
160
186
  changed.has('opened') ||
161
187
  changed.has('defaultDepth')) {
162
188
  this.rows = this.flatten();
163
- this.active = Math.min(this.active, Math.max(0, this.rows.length - 1));
189
+ const found = was === undefined ? -1 : this.rows.findIndex((row) => row.path === was);
190
+ this.active = found >= 0 ? found : Math.min(this.active, Math.max(0, this.rows.length - 1));
164
191
  }
165
192
  }
166
193
  firstUpdated(changed) {
@@ -229,6 +256,35 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
229
256
  this.active = Math.max(0, Math.min(to, last));
230
257
  this.revealing = true;
231
258
  }
259
+ openSiblings(row) {
260
+ const parent = row.path.slice(0, row.path.lastIndexOf('/'));
261
+ const opened = new Set(this.opened);
262
+ for (const one of this.rows) {
263
+ if (one.branch &&
264
+ one.depth === row.depth &&
265
+ one.path.slice(0, one.path.lastIndexOf('/')) === parent) {
266
+ opened.add(one.path);
267
+ }
268
+ }
269
+ this.opened = opened;
270
+ }
271
+ typeahead(letter) {
272
+ const now = Date.now();
273
+ const text = now - this.typed.at < 500 ? this.typed.text + letter : letter;
274
+ this.typed = { at: now, text };
275
+ const wanted = text.toLocaleLowerCase();
276
+ const repeated = [...wanted].every((one) => one === wanted[0]);
277
+ const rows = this.rows;
278
+ const start = wanted.length === 1 || repeated ? this.active + 1 : this.active;
279
+ for (let step = 0; step < rows.length; step += 1) {
280
+ const at = (start + step) % rows.length;
281
+ const key = rows[at]?.key?.toLocaleLowerCase();
282
+ if (key?.startsWith(repeated ? (wanted[0] ?? '') : wanted)) {
283
+ this.move(at);
284
+ return;
285
+ }
286
+ }
287
+ }
232
288
  handleRowClick(index, row) {
233
289
  this.active = index;
234
290
  if (row.branch) {
@@ -240,9 +296,18 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
240
296
  if (row.branch) {
241
297
  const count = entries(value).length;
242
298
  const array = Array.isArray(value);
243
- const noun = array ? (count === 1 ? 'item' : 'items') : count === 1 ? 'key' : 'keys';
299
+ const one = new Intl.PluralRules(this.lang || undefined).select(count) === 'one';
300
+ const words = array
301
+ ? one
302
+ ? this.itemLabel
303
+ : this.itemsLabel
304
+ : one
305
+ ? this.keyLabel
306
+ : this.keysLabel;
244
307
  return html `<span part="summary">${array ? '[…]' : '{…}'}</span>
245
- <span part="count" dir="auto">${`${count} ${noun}`}</span>`;
308
+ <span part="count" dir="auto"
309
+ >${fillTemplate(words, { count: formatNumbers(this.lang)(count) })}</span
310
+ >`;
246
311
  }
247
312
  if (typeof value === 'string') {
248
313
  return html `<span part="string" dir="ltr">${JSON.stringify(value)}</span>`;
@@ -310,7 +375,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
310
375
  role="tree"
311
376
  tabindex="0"
312
377
  ?data-scroller=${this.windowed}
313
- aria-label=${this.label || 'Value'}
378
+ aria-label=${this.label || this.treeLabel}
314
379
  aria-activedescendant=${rows.length > 0 ? `row-${this.active}` : nothing}
315
380
  @keydown=${this.handleKeydown}
316
381
  @scroll=${() => this.virtualize.measure()}
@@ -335,12 +400,27 @@ __decorate([
335
400
  __decorate([
336
401
  property()
337
402
  ], HelixHeadlessObjectInspector.prototype, "for", void 0);
403
+ __decorate([
404
+ property({ attribute: 'item-label' })
405
+ ], HelixHeadlessObjectInspector.prototype, "itemLabel", void 0);
406
+ __decorate([
407
+ property({ attribute: 'items-label' })
408
+ ], HelixHeadlessObjectInspector.prototype, "itemsLabel", void 0);
409
+ __decorate([
410
+ property({ attribute: 'key-label' })
411
+ ], HelixHeadlessObjectInspector.prototype, "keyLabel", void 0);
412
+ __decorate([
413
+ property({ attribute: 'keys-label' })
414
+ ], HelixHeadlessObjectInspector.prototype, "keysLabel", void 0);
338
415
  __decorate([
339
416
  property()
340
417
  ], HelixHeadlessObjectInspector.prototype, "label", void 0);
341
418
  __decorate([
342
419
  property({ attribute: 'no-copy', type: Boolean })
343
420
  ], HelixHeadlessObjectInspector.prototype, "noCopy", void 0);
421
+ __decorate([
422
+ property({ attribute: 'tree-label' })
423
+ ], HelixHeadlessObjectInspector.prototype, "treeLabel", void 0);
344
424
  __decorate([
345
425
  property({ attribute: false })
346
426
  ], HelixHeadlessObjectInspector.prototype, "value", void 0);
@@ -53,13 +53,16 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
53
53
  * @csspart label - The option's label.
54
54
  * @csspart text - The label and the description under it, as one column.
55
55
  *
56
+ * @cssstate active - While the keyboard is on the option, which is the option `aria-activedescendant` names.
56
57
  * @cssstate selected - While the option is chosen.
57
58
  */
58
59
  export declare class HelixHeadlessOption extends HelixElement {
59
60
  /**
60
61
  * Whether this is the option the keyboard is on.
61
62
  *
62
- * Written by the listbox.
63
+ * Written by the listbox, which writes it once a reader moves the highlight.
64
+ * It shows as `:state(active)`, and as a `data-active` attribute for the
65
+ * stylesheets written before the state existed.
63
66
  */
64
67
  active: boolean;
65
68
  /**
@@ -44,6 +44,7 @@ let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
44
44
  willUpdate(changed) {
45
45
  super.willUpdate(changed);
46
46
  this.toggleState('selected', this.selected);
47
+ this.toggleState('active', this.active);
47
48
  }
48
49
  render() {
49
50
  this.internals.role = 'option';
@@ -11,6 +11,10 @@ import { type PropertyValues, type TemplateResult } from 'lit';
11
11
  * render one inside the outline's shadow root, and the hrefs are fragments on
12
12
  * the same page.
13
13
  *
14
+ * Write the anchors flat, with `data-level` for their depth, or nested in
15
+ * lists, `<ul><li><a>`. A screen reader hears a nested list's levels; it hears
16
+ * `data-level` as a flat run of links.
17
+ *
14
18
  * The current section is decided by the headings' positions. An
15
19
  * `IntersectionObserver` with a band near the top fails both ways: when
16
20
  * headings are far apart, none is in the band and no section is marked, and
@@ -22,7 +26,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
22
26
  * @aliases Table of contents, On this page, Outline
23
27
  * @documentation https://fusion.dev/helix/components/outline
24
28
  *
25
- * @slot - The links, as anchors with fragment hrefs.
29
+ * @slot - The links, as anchors with fragment hrefs, or lists holding them.
26
30
  *
27
31
  * @event {HelixActivateEvent} helix-activate - Fired when the section in view changes, with its id as the value. It reports the scroll position and cannot be prevented.
28
32
  *
@@ -88,7 +92,14 @@ export declare class HelixHeadlessOutline extends HelixElement {
88
92
  */
89
93
  private reading;
90
94
  private observer;
91
- /** The links, in document order. Empty on the server, which has no tree. */
95
+ /**
96
+ * The links, in document order. Empty on the server, which has no tree.
97
+ *
98
+ * **Anchors inside what is slotted count too.** A contents list is written
99
+ * `<ul><li><a>` more often than as a run of anchors, and reading only the
100
+ * anchors assigned to the slot found no sections in it: nothing was marked,
101
+ * nothing was announced, and nothing said why.
102
+ */
92
103
  private get links();
93
104
  /**
94
105
  * What each link points at, paired with the link, skipping anything missing.
@@ -78,8 +78,10 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
78
78
  }
79
79
  get links() {
80
80
  const slot = this.renderRoot?.querySelector('slot');
81
- return (slot?.assignedElements({ flatten: true }) ?? []).filter((element) => element instanceof HTMLAnchorElement &&
82
- element.getAttribute('href')?.startsWith('#') === true);
81
+ const fragment = (element) => element instanceof HTMLAnchorElement && element.getAttribute('href')?.startsWith('#') === true;
82
+ return (slot?.assignedElements({ flatten: true }) ?? []).flatMap((element) => fragment(element)
83
+ ? [element]
84
+ : [...element.querySelectorAll('a[href^="#"]')].filter(fragment));
83
85
  }
84
86
  get sections() {
85
87
  return this.links.flatMap((link) => {
@@ -39,7 +39,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
39
39
  * @slot next-icon - The glyph on the forward-one-page control, usually a caret.
40
40
  * @slot previous-icon - The glyph on the back-one-page control, usually a caret.
41
41
  *
42
- * @event {HelixNavigateEvent} helix-navigate - Fired before the page changes, and cancelable. Canceling it is the only way to stop a page change.
42
+ * @event {HelixActivateEvent} helix-activate - Fired after the page changes, with the new page as a string in `detail.value`. In `href` mode it comes a task after the click, once the page has moved.
43
+ * @event {HelixNavigateEvent} helix-navigate - Fired before the page changes, and cancelable. Canceling it is the only way to stop a page change. `detail.page` is the page asked for, as a number, and `detail.value` is the same page as a string.
43
44
  *
44
45
  * @csspart first - The control that jumps to the first page, under `ends`.
45
46
  * @csspart gap - The control in place of hidden pages, which jumps into them.
@@ -218,6 +219,11 @@ export declare class HelixHeadlessPagination extends HelixElement {
218
219
  private announced;
219
220
  /** The page on show, whoever set it, and `undefined` until somebody has. */
220
221
  private pageState;
222
+ /**
223
+ * The control a reader just used, while it held focus, and whether it was a
224
+ * gap. Read once by the update that follows, which decides where focus goes.
225
+ */
226
+ private used;
221
227
  /** Held across every component asking for the same locale. */
222
228
  private get numbers();
223
229
  /**
@@ -263,6 +269,18 @@ export declare class HelixHeadlessPagination extends HelixElement {
263
269
  * for both shapes rather than two that have to stay in step.
264
270
  */
265
271
  private readonly go;
272
+ private applyPage;
273
+ /**
274
+ * Puts focus back in the row after the control a reader used has gone.
275
+ *
276
+ * A page keeps its own control across a move, because the run is keyed by
277
+ * page, so focus stays on the page that was chosen. A gap points somewhere
278
+ * new once the run moves, and a step control is disabled at the end it
279
+ * reached, so after either of those focus goes to the current page, or in
280
+ * `compact` to the first step control that can still be used. Left alone,
281
+ * focus fell to the top of the document.
282
+ */
283
+ private restoreFocus;
266
284
  protected willUpdate(changed: PropertyValues): void;
267
285
  /**
268
286
  * Says where the reader now is, once they have moved.