@fusion.dev/helix-headless 0.147.0 → 0.148.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 (141) hide show
  1. package/custom-elements.json +4512 -790
  2. package/dist/accordion/item/accordion-item.d.ts +5 -2
  3. package/dist/accordion/item/accordion-item.js +11 -7
  4. package/dist/accordion/root/accordion-root.js +4 -6
  5. package/dist/announcement/root/announcement-root.d.ts +56 -5
  6. package/dist/announcement/root/announcement-root.js +124 -21
  7. package/dist/area-chart/root/area-chart-root.js +3 -4
  8. package/dist/badge/count.d.ts +14 -0
  9. package/dist/badge/count.js +20 -0
  10. package/dist/badge/root/badge-root.js +8 -22
  11. package/dist/blockquote/root/blockquote-root.d.ts +66 -8
  12. package/dist/blockquote/root/blockquote-root.js +85 -8
  13. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +2 -2
  14. package/dist/button-group/arrows.d.ts +14 -0
  15. package/dist/button-group/arrows.js +10 -0
  16. package/dist/button-group/root/button-group-root.d.ts +141 -3
  17. package/dist/button-group/root/button-group-root.js +142 -2
  18. package/dist/canvas/edge-pan.d.ts +61 -0
  19. package/dist/canvas/edge-pan.js +63 -0
  20. package/dist/canvas/node/canvas-node.d.ts +12 -2
  21. package/dist/canvas/node/canvas-node.js +6 -2
  22. package/dist/canvas/root/canvas-document.d.ts +10 -0
  23. package/dist/canvas/root/canvas-document.js +1 -0
  24. package/dist/canvas/root/canvas-root.d.ts +107 -5
  25. package/dist/canvas/root/canvas-root.js +140 -36
  26. package/dist/canvas/snap.d.ts +42 -0
  27. package/dist/canvas/snap.js +32 -0
  28. package/dist/carousel/root/carousel-root.d.ts +4 -3
  29. package/dist/carousel/root/carousel-root.js +2 -6
  30. package/dist/chat/root/chat-root.d.ts +13 -3
  31. package/dist/chat/root/chat-root.js +2 -6
  32. package/dist/checkbox-group/root/checkbox-group-root.d.ts +13 -0
  33. package/dist/checkbox-group/root/checkbox-group-root.js +18 -10
  34. package/dist/checkbox-group/select-all.d.ts +11 -0
  35. package/dist/checkbox-group/select-all.js +5 -0
  36. package/dist/code-block/root/code-block-root.d.ts +8 -6
  37. package/dist/code-block/root/code-block-root.js +4 -3
  38. package/dist/color-picker/root/color-picker-root.js +6 -12
  39. package/dist/color-picker/wheel.d.ts +11 -0
  40. package/dist/color-picker/wheel.js +9 -0
  41. package/dist/combobox/root/combobox-root.d.ts +70 -11
  42. package/dist/combobox/root/combobox-root.js +68 -18
  43. package/dist/command/root/command-root.d.ts +4 -3
  44. package/dist/data-table/root/data-table-root.js +17 -44
  45. package/dist/description-list/item/description-list-item.d.ts +4 -0
  46. package/dist/description-list/item/description-list-item.js +7 -1
  47. package/dist/divider/root/divider-root.d.ts +45 -16
  48. package/dist/divider/root/divider-root.js +28 -4
  49. package/dist/drilldown/item/drilldown-item.js +3 -4
  50. package/dist/element-events.generated.js +1 -0
  51. package/dist/empty/root/empty-root.d.ts +17 -3
  52. package/dist/empty/root/empty-root.js +29 -5
  53. package/dist/file-input/root/file-input-root.js +3 -4
  54. package/dist/file-input/status.js +2 -1
  55. package/dist/filter-input/chip/filter-chip.js +2 -3
  56. package/dist/format-date/fields.d.ts +20 -0
  57. package/dist/format-date/fields.js +14 -0
  58. package/dist/format-date/options.d.ts +2 -1
  59. package/dist/format-date/options.js +9 -38
  60. package/dist/format-date/root/format-date-root.d.ts +35 -3
  61. package/dist/format-date/root/format-date-root.js +24 -32
  62. package/dist/graph/root/graph-root.d.ts +23 -5
  63. package/dist/graph/root/graph-root.js +17 -20
  64. package/dist/heatmap/root/heatmap-root.d.ts +12 -0
  65. package/dist/heatmap/root/heatmap-root.js +26 -11
  66. package/dist/image/root/image-root.d.ts +11 -0
  67. package/dist/kbd/root/kbd-root.js +1 -2
  68. package/dist/legend/root/legend-root.js +4 -17
  69. package/dist/list/root/list-root.js +8 -22
  70. package/dist/listbox/root/listbox-root.d.ts +119 -24
  71. package/dist/listbox/root/listbox-root.js +130 -132
  72. package/dist/map/marker/map-marker.d.ts +2 -1
  73. package/dist/map/root/map-root.d.ts +40 -2
  74. package/dist/map/root/map-root.js +64 -23
  75. package/dist/megamenu/root/megamenu-root.js +4 -11
  76. package/dist/menu/root/menu-root.d.ts +2 -2
  77. package/dist/navigation/root/navigation-root.js +5 -8
  78. package/dist/notifications/group/notification-group.d.ts +8 -1
  79. package/dist/notifications/group/notification-group.js +12 -6
  80. package/dist/notifications/notification/notification.d.ts +12 -1
  81. package/dist/notifications/notification/notification.js +3 -1
  82. package/dist/notifications/root/notifications-root.d.ts +41 -5
  83. package/dist/notifications/root/notifications-root.js +27 -7
  84. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -1
  85. package/dist/object-inspector/root/object-inspector-root.js +21 -11
  86. package/dist/pagination/size/pagination-size.d.ts +1 -0
  87. package/dist/pagination/size/pagination-size.js +1 -1
  88. package/dist/pin-input/code.d.ts +23 -0
  89. package/dist/pin-input/code.js +25 -0
  90. package/dist/pin-input/root/pin-input-root.js +9 -23
  91. package/dist/qr-code/root/qr-code-root.d.ts +14 -3
  92. package/dist/qr-code/root/qr-code-root.js +19 -5
  93. package/dist/scatter-chart/measures.d.ts +22 -0
  94. package/dist/scatter-chart/measures.js +11 -0
  95. package/dist/scatter-chart/root/scatter-chart-root.d.ts +2 -6
  96. package/dist/scatter-chart/root/scatter-chart-root.js +6 -19
  97. package/dist/select/root/select-root.d.ts +56 -8
  98. package/dist/select/root/select-root.js +33 -9
  99. package/dist/speed-dial/group/speed-dial-group.d.ts +48 -0
  100. package/dist/speed-dial/group/speed-dial-group.js +43 -0
  101. package/dist/speed-dial/index.d.ts +1 -0
  102. package/dist/speed-dial/index.js +1 -0
  103. package/dist/speed-dial/item/speed-dial-item.d.ts +26 -1
  104. package/dist/speed-dial/item/speed-dial-item.js +41 -3
  105. package/dist/speed-dial/long-press.d.ts +50 -0
  106. package/dist/speed-dial/long-press.js +48 -0
  107. package/dist/speed-dial/root/speed-dial-root.d.ts +80 -1
  108. package/dist/speed-dial/root/speed-dial-root.js +114 -5
  109. package/dist/splitter/root/splitter-root.d.ts +7 -3
  110. package/dist/splitter/root/splitter-root.js +16 -3
  111. package/dist/table/cell/table-cell.d.ts +8 -1
  112. package/dist/table/cell/table-cell.js +6 -2
  113. package/dist/table/grid-controller.d.ts +117 -0
  114. package/dist/table/grid-controller.js +293 -0
  115. package/dist/table/root/table-root.d.ts +47 -3
  116. package/dist/table/root/table-root.js +81 -24
  117. package/dist/table/row/table-row.d.ts +13 -1
  118. package/dist/table/row/table-row.js +20 -6
  119. package/dist/tag/group/tag-group.js +6 -15
  120. package/dist/time-picker/root/time-picker-root.d.ts +1 -0
  121. package/dist/time-picker/root/time-picker-root.js +3 -0
  122. package/dist/toast/item/toast-item.d.ts +6 -3
  123. package/dist/toast/item/toast-item.js +1 -0
  124. package/dist/toast/root/toast-root.d.ts +13 -1
  125. package/dist/toast/root/toast-root.js +20 -14
  126. package/dist/toggle/group/toggle-group.js +3 -5
  127. package/dist/toolbar/root/toolbar-root.d.ts +31 -0
  128. package/dist/toolbar/root/toolbar-root.js +39 -8
  129. package/dist/tooltip/root/tooltip-root.js +1 -2
  130. package/dist/video/root/video-root.d.ts +11 -0
  131. package/dist/video/root/video-root.js +15 -1
  132. package/dist/virtual-list/root/virtual-list-root.js +6 -18
  133. package/lint.json +1 -1
  134. package/package.json +5 -5
  135. package/types/jsx.d.ts +2005 -266
  136. package/types/svelte.d.ts +1 -0
  137. package/types/vue.d.ts +338 -62
  138. package/dist/kbd/keys.d.ts +0 -29
  139. package/dist/kbd/keys.js +0 -52
  140. package/dist/tooltip/focus-target.d.ts +0 -6
  141. package/dist/tooltip/focus-target.js +0 -9
@@ -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 { AriaForwardController, CopyController, countLabel, customElement, drawStatus, elementById, fillTemplate, formatNumbers, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeCloseEvent, HelixBeforeOpenEvent, HelixElement, HelixFilterEvent, HelixFilteredEvent, inlineStep, requestChange, StatusController, scrollItemIntoView, searchableById, Typeahead, treeArrow, VirtualizeController, } from '@fusion.dev/helix-core';
8
+ import { AriaForwardController, CopyController, countLabel, customElement, drawStatus, elementById, fillTemplate, formatNumbers, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeCloseEvent, HelixBeforeOpenEvent, HelixElement, HelixFilterEvent, HelixFilteredEvent, inlineStep, pageRows, requestChange, StatusController, scrollItemIntoView, scrollRoom, searchableById, Typeahead, treeArrow, 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';
@@ -85,10 +85,10 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
85
85
  this.move(rows.length - 1);
86
86
  break;
87
87
  case 'PageDown':
88
- this.move(this.cursor + this.pageRows());
88
+ this.move(this.cursor + this.pageStep());
89
89
  break;
90
90
  case 'PageUp':
91
- this.move(this.cursor - this.pageRows());
91
+ this.move(this.cursor - this.pageStep());
92
92
  break;
93
93
  case 'ArrowRight':
94
94
  case 'ArrowLeft':
@@ -153,6 +153,11 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
153
153
  this.requestUpdate();
154
154
  }
155
155
  };
156
+ this.handleTreeFocus = () => {
157
+ this.revealing = true;
158
+ this.focusing = true;
159
+ this.requestUpdate();
160
+ };
156
161
  this.handleFocusIn = (event) => {
157
162
  const index = Number(event.target.dataset?.index);
158
163
  if (Number.isInteger(index) && index !== this.cursor) {
@@ -367,6 +372,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
367
372
  }
368
373
  this.revealing = true;
369
374
  this.focusing = true;
375
+ this.requestUpdate();
370
376
  return true;
371
377
  }
372
378
  typeahead(letter) {
@@ -375,11 +381,8 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
375
381
  this.move(at);
376
382
  }
377
383
  }
378
- pageRows() {
379
- const tree = this.partNamed('tree');
380
- const room = tree && tree.scrollHeight > tree.clientHeight + 1 ? tree.clientHeight : window.innerHeight;
381
- const tall = this.virtualize.sizeOf(this.cursor) || HelixHeadlessObjectInspector_1.ESTIMATE;
382
- return Math.max(1, Math.floor(room / tall) - 1);
384
+ pageStep() {
385
+ return pageRows(scrollRoom(this.partNamed('tree')), this.virtualize.sizeOf(this.cursor) || HelixHeadlessObjectInspector_1.ESTIMATE);
383
386
  }
384
387
  handleRowClick(index, row) {
385
388
  if (this.move(index) && row.branch) {
@@ -446,8 +449,11 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
446
449
  </div>`;
447
450
  }
448
451
  hasFocusedRow() {
452
+ return this.focusedIndex() !== undefined;
453
+ }
454
+ focusedIndex() {
449
455
  const focused = this.shadowRoot?.activeElement;
450
- return focused?.getAttribute?.('role') === 'row';
456
+ return focused?.getAttribute?.('role') === 'row' ? Number(focused.dataset.index) : undefined;
451
457
  }
452
458
  renderCopyButton(row) {
453
459
  return staticHtml `<${this.tag('copy-button')}
@@ -464,8 +470,9 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
464
470
  render() {
465
471
  const rows = this.rows;
466
472
  const { first, last } = this.windowed ? this.virtualize.window : { first: 0, last: rows.length };
467
- const kept = this.windowed && (this.cursor < first || this.cursor >= last) && this.hasFocusedRow()
468
- ? this.cursor
473
+ const focused = this.focusedIndex();
474
+ const kept = this.windowed && focused !== undefined && (focused < first || focused >= last)
475
+ ? focused
469
476
  : undefined;
470
477
  const { after, before } = this.windowed
471
478
  ? this.virtualize.spacers(kept)
@@ -475,6 +482,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
475
482
  if (kept !== undefined && keptRow) {
476
483
  shown.splice(kept < first ? 0 : shown.length, 0, [keptRow, kept]);
477
484
  }
485
+ const stopless = rows.length > 0 && !shown.some(([, at]) => at === this.cursor);
478
486
  return html `${this.label ? html `<div part="header"><span part="label">${this.label}</span></div>` : nothing}${this.filterable
479
487
  ? html `<input
480
488
  part="search-field"
@@ -494,8 +502,10 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
494
502
  id="tree"
495
503
  role="treegrid"
496
504
  ?data-scroller=${this.windowed}
505
+ tabindex=${stopless ? '0' : nothing}
497
506
  aria-label=${this.label || this.treeLabel}
498
507
  @keydown=${this.handleKeydown}
508
+ @focus=${this.handleTreeFocus}
499
509
  @focusin=${this.handleFocusIn}
500
510
  @focusout=${this.handleFocusOut}
501
511
  @scroll=${() => this.virtualize.measure()}
@@ -65,6 +65,7 @@ import '../../select/index.js';
65
65
  * @csspart control-listbox-loading - The text shown while the options load.
66
66
  * @csspart control-listbox-search - The region that holds the filter field.
67
67
  * @csspart control-listbox-search-field - The filter field.
68
+ * @csspart control-live - The select's `live` part, the region a screen reader hears a choice made on the closed trigger from.
68
69
  * @csspart control-surface - The floating panel.
69
70
  * @csspart control-trigger - The closed control, which shows the chosen size.
70
71
  * @csspart control-value - The chosen size inside the trigger.
@@ -98,7 +98,7 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
98
98
  listbox-filter-mark: control-listbox-filter-mark, listbox-hint: control-listbox-hint,
99
99
  listbox-items: control-listbox-items, listbox-label: control-listbox-label,
100
100
  listbox-live: control-listbox-live, listbox-loading: control-listbox-loading, listbox-search: control-listbox-search,
101
- listbox-search-field: control-listbox-search-field, surface: control-surface,
101
+ listbox-search-field: control-listbox-search-field, live: control-live, surface: control-surface,
102
102
  trigger: control-trigger, value: control-value"
103
103
  label=${named ? '' : this.label}
104
104
  aria-label=${named || nothing}
@@ -1,3 +1,4 @@
1
+ import { type PickerVerdict } from '@fusion.dev/helix-core';
1
2
  /** What a box accepts, which decides the keyboard a phone offers. */
2
3
  export type PinType = 'alphanumeric' | 'numeric' | 'text';
3
4
  /** How typed letters are cased: as typed, or turned upper or lower case. */
@@ -35,3 +36,25 @@ export declare const readTyped: (raw: string, caret: number, type: PinType, lett
35
36
  * holding two fill from box three.
36
37
  */
37
38
  export declare const overwriteCode: (code: string, at: number, text: string, count: number) => CodeEdit;
39
+ /** The messages a code's verdict is written with, where the page set its own. */
40
+ export interface CodeMessages {
41
+ /** For a code with no characters in a required field. */
42
+ missing?: string | undefined;
43
+ /** For a code with fewer characters than boxes; `{length}` is how many. */
44
+ short?: string | undefined;
45
+ }
46
+ /**
47
+ * What is wrong with a code, as a picker field reports it: a required field
48
+ * with nothing in it is missing, a code with fewer characters than boxes is
49
+ * too short, and anything else is fine (`''`). The platform's own words where
50
+ * the page set none.
51
+ */
52
+ export declare const codeVerdict: (code: string, count: number, required: boolean, messages: CodeMessages) => PickerVerdict;
53
+ /**
54
+ * The box a caret key moves to from `caret`, kept within the boxes up to
55
+ * `end`, or `undefined` for a key that moves nothing.
56
+ *
57
+ * Not `inlineStep`: the row is left to right whatever the page, so the key
58
+ * pointing right is the one that moves on in every script.
59
+ */
60
+ export declare const caretTo: (key: string, caret: number, end: number) => number | undefined;
@@ -1,3 +1,4 @@
1
+ import { fillTemplate } from '@fusion.dev/helix-core';
1
2
  export const cleanCode = (text, type, letterCase = 'preserve') => {
2
3
  const trimmed = text.trim();
3
4
  const kept = type === 'numeric'
@@ -23,3 +24,27 @@ export const overwriteCode = (code, at, text, count) => {
23
24
  }
24
25
  return { caret: Math.min(count - 1, start + text.length), code: characters.join('') };
25
26
  };
27
+ export const codeVerdict = (code, count, required, messages) => {
28
+ if (required && code === '') {
29
+ return ['valueMissing', messages.missing || 'Please fill in this field.'];
30
+ }
31
+ if (code !== '' && code.length < count) {
32
+ return [
33
+ 'tooShort',
34
+ fillTemplate(messages.short || 'Please enter all {length} characters.', { length: count }),
35
+ ];
36
+ }
37
+ return '';
38
+ };
39
+ export const caretTo = (key, caret, end) => {
40
+ const to = key === 'ArrowRight'
41
+ ? caret + 1
42
+ : key === 'ArrowLeft'
43
+ ? caret - 1
44
+ : key === 'Home'
45
+ ? 0
46
+ : key === 'End'
47
+ ? end
48
+ : undefined;
49
+ return to === undefined ? undefined : Math.max(0, Math.min(to, end));
50
+ };
@@ -4,10 +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 { AriaForwardController, customElement, fillTemplate, HelixCompleteEvent, HelixFormElement, MixinControllerValue, MixinField, restoreValue, submitImplicitly, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixCompleteEvent, HelixFormElement, MixinControllerValue, MixinField, restoreValue, submitImplicitly, verdictFlags, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
- import { cleanCode, overwriteCode, readTyped, } from '../code.js';
10
+ import { caretTo, cleanCode, codeVerdict, overwriteCode, readTyped, } from '../code.js';
11
11
  import { separatorsAfter } from '../format.js';
12
12
  const UNSEEN = 'position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;font-size:16px';
13
13
  const keepFocus = (event) => event.preventDefault();
@@ -61,20 +61,12 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
61
61
  }
62
62
  return;
63
63
  }
64
- const to = event.key === 'ArrowRight'
65
- ? this.caretBox + 1
66
- : event.key === 'ArrowLeft'
67
- ? this.caretBox - 1
68
- : event.key === 'Home'
69
- ? 0
70
- : event.key === 'End'
71
- ? this.end
72
- : undefined;
64
+ const to = caretTo(event.key, this.caretBox, this.end);
73
65
  if (to === undefined) {
74
66
  return;
75
67
  }
76
68
  event.preventDefault();
77
- this.active = Math.max(0, Math.min(to, this.end));
69
+ this.active = to;
78
70
  this.placeCaret(true);
79
71
  };
80
72
  this.handlePress = (event) => {
@@ -172,17 +164,11 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
172
164
  if (!control) {
173
165
  return;
174
166
  }
175
- if (this.required && this.value === '') {
176
- this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage || 'Please fill in this field.', control);
177
- }
178
- else if (this.value !== '' && this.value.length < this.count) {
179
- this.internals.setValidity({ tooShort: true }, fillTemplate(this.tooShortMessage || 'Please enter all {length} characters.', {
180
- length: this.count,
181
- }), control);
182
- }
183
- else {
184
- this.internals.setValidity({}, '', control);
185
- }
167
+ const found = verdictFlags(codeVerdict(this.value, this.count, this.required, {
168
+ missing: this.valueMissingMessage,
169
+ short: this.tooShortMessage,
170
+ }));
171
+ this.internals.setValidity(found?.[0] ?? {}, found?.[1] ?? '', control);
186
172
  this.refreshFeedback();
187
173
  }
188
174
  focus(options) {
@@ -27,7 +27,8 @@ export type QrStyle = 'dots' | 'rounded' | 'square';
27
27
  * `correction` sets how much of the code can be lost. At `H` a code can lose
28
28
  * thirty percent and still scan, so a mark in the center slot is safe. At `L`
29
29
  * the same mark makes the code unscannable. The default is `M`, and `H` when the
30
- * `logo` slot is filled.
30
+ * `logo` slot is filled. A code that holds a logo at a `correction` below `H`
31
+ * warns once with the kind `unscannable-logo`.
31
32
  *
32
33
  * A higher level holds fewer characters, so a value that fits at `M` can be too
33
34
  * long once a logo raises the level to `H`. A value too long for the level
@@ -37,7 +38,8 @@ export type QrStyle = 'dots' | 'rounded' | 'square';
37
38
  * `toSvg()`, `toPng()` and `download()` give the drawn code as a file of its
38
39
  * own, in the colors it is drawn in, with a slotted image or SVG logo where
39
40
  * it sits, for a page that offers it to save or print, as Zag's download
40
- * trigger does.
41
+ * trigger does. `download()` saves an SVG for a name ending in `.svg`, and a
42
+ * PNG for any other.
41
43
  *
42
44
  * @since 0.1.0
43
45
  * @status stable
@@ -111,6 +113,14 @@ export declare class HelixHeadlessQrCode extends HelixElement {
111
113
  protected firstUpdated(changed: PropertyValues): void;
112
114
  /** The level actually used, which the slot can raise. */
113
115
  private get level();
116
+ /** Whether this code has warned that its logo may keep it from scanning. */
117
+ private toldLogo;
118
+ /**
119
+ * Warns once when a logo covers a code whose level was set below `H`, where
120
+ * the cover can take more than the level can lose. A level the code chose
121
+ * itself is `H` already.
122
+ */
123
+ private auditLogo;
114
124
  /** The value and level that did not fit, so `error` fires once for each. */
115
125
  private overflowed;
116
126
  /**
@@ -148,7 +158,8 @@ export declare class HelixHeadlessQrCode extends HelixElement {
148
158
  */
149
159
  toPng(size?: number): Promise<Blob | null>;
150
160
  /**
151
- * Saves the code as a PNG named `filename`, `size` pixels square.
161
+ * Saves the code as a file named `filename`, `size` pixels square: an SVG
162
+ * where the name ends in `.svg`, and a PNG otherwise.
152
163
  *
153
164
  * Call it from a press, since a browser lets a page start a download only
154
165
  * in answer to one. Does nothing where no code is drawn.
@@ -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, rgbOf } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, reportRuntime, rgbOf, slotFilled, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg, } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { encode } from 'uqr';
@@ -81,6 +81,7 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
81
81
  this.label = '';
82
82
  this.value = '';
83
83
  this.logo = false;
84
+ this.toldLogo = false;
84
85
  this.overflowed = '';
85
86
  }
86
87
  willUpdate(changed) {
@@ -96,6 +97,17 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
96
97
  get level() {
97
98
  return this.correction ?? (this.logo ? 'H' : 'M');
98
99
  }
100
+ auditLogo() {
101
+ if (this.toldLogo || !this.logo || (this.correction ?? 'H') === 'H') {
102
+ return;
103
+ }
104
+ this.toldLogo = true;
105
+ reportRuntime({
106
+ kind: 'unscannable-logo',
107
+ message: `<${this.localName}> holds a logo at correction "${this.correction}", which may keep it from scanning. Set \`correction\` to "H", or leave it unset.`,
108
+ tag: this.localName,
109
+ });
110
+ }
99
111
  encoded() {
100
112
  try {
101
113
  return encode(this.value, { border: 0, ecc: this.level });
@@ -109,6 +121,7 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
109
121
  }
110
122
  updated(changed) {
111
123
  super.updated(changed);
124
+ this.auditLogo();
112
125
  const key = `${this.level}:${this.value}`;
113
126
  const overflow = this.value !== '' && this.encoded() === undefined;
114
127
  this.toggleState('overflow', overflow);
@@ -186,11 +199,12 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
186
199
  return new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
187
200
  }
188
201
  async download(filename = 'qr-code.png', size = 512) {
189
- const png = await this.toPng(size);
190
- if (!png) {
202
+ const svg = /\.svg$/i.test(filename) ? this.toSvg(size) : '';
203
+ const file = svg ? new Blob([svg], { type: 'image/svg+xml' }) : await this.toPng(size);
204
+ if (!file) {
191
205
  return;
192
206
  }
193
- const url = URL.createObjectURL(png);
207
+ const url = URL.createObjectURL(file);
194
208
  const link = document.createElement('a');
195
209
  link.href = url;
196
210
  link.download = filename;
@@ -235,7 +249,7 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
235
249
  <slot
236
250
  name="logo"
237
251
  @slotchange=${(event) => {
238
- const next = event.target.assignedNodes().length > 0;
252
+ const next = slotFilled(event.target);
239
253
  if (next !== this.logo) {
240
254
  this.logo = next;
241
255
  this.requestUpdate();
@@ -0,0 +1,22 @@
1
+ /** The numeric columns by what they measure. */
2
+ export interface Measures {
3
+ /** The column across, the first one that is not the size. */
4
+ across: number | undefined;
5
+ /** The column `size-by` names, which sizes the points. */
6
+ size: number | undefined;
7
+ /** Every other column, each a series up. */
8
+ ups: number[];
9
+ }
10
+ /**
11
+ * The numeric columns by what they measure: the column whose heading
12
+ * `sizeBy` names as the size, by its series key, then the first of the rest
13
+ * across and every other one up.
14
+ */
15
+ export declare const measuresOf: (columns: readonly number[], name: (column: number) => string, sizeBy: string) => Measures;
16
+ /**
17
+ * Each row's point size from its reading in the size column, by area, so a
18
+ * point twice the measure is twice the ink: the square root of its share of
19
+ * the largest. A size below zero is none, and a row with no size, or a column
20
+ * whose largest is not above zero, has no weight.
21
+ */
22
+ export declare const bubbleWeights: (sizes: readonly (number | undefined)[]) => (number | undefined)[];
@@ -0,0 +1,11 @@
1
+ import { seriesKey } from '@fusion.dev/helix-core';
2
+ export const measuresOf = (columns, name, sizeBy) => {
3
+ const key = seriesKey(sizeBy);
4
+ const size = key === '' ? undefined : columns.find((column) => seriesKey(name(column)) === key);
5
+ const [across, ...ups] = columns.filter((column) => column !== size);
6
+ return { across, size, ups };
7
+ };
8
+ export const bubbleWeights = (sizes) => {
9
+ const largest = Math.max(0, ...sizes.map((one) => one ?? 0));
10
+ return sizes.map((one) => one === undefined || largest <= 0 ? undefined : Math.sqrt(Math.max(0, one) / largest));
11
+ };
@@ -1,6 +1,7 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type TemplateResult } from 'lit';
3
3
  import '../../tooltip/index.js';
4
+ import { type Measures } from '../measures.js';
4
5
  /** One row drawn as a point: which row, which series, and where, in percent of the plot. */
5
6
  export interface ScatterPoint {
6
7
  depth: number;
@@ -172,12 +173,7 @@ export declare class HelixHeadlessScatterChart extends HelixHeadlessScatterChart
172
173
  * The numeric columns by what they measure: the first across, the column
173
174
  * `size-by` names as the size, and every other one up, as a series.
174
175
  */
175
- protected get measures(): {
176
- across: number | undefined;
177
- size: number | undefined;
178
- ups: number[];
179
- };
180
- showsColumn(cell: number): boolean;
176
+ protected get measures(): Measures;
181
177
  /** Whether the levels across are ruled. */
182
178
  protected get rulesX(): boolean;
183
179
  /** Whether the levels up are ruled. */
@@ -4,11 +4,12 @@ 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 { CHART_PLOT, customElement, drawTooltip, extent, formatNumbers, HelixElement, isRtl, linear, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesKey, ticks, } from '@fusion.dev/helix-core';
7
+ import { CHART_PLOT, customElement, drawTooltip, extent, formatNumbers, HelixElement, isRtl, linear, MixinChartSource, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, ticks, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
11
11
  import { correlation, leastSquares } from '../fit.js';
12
+ import { bubbleWeights, measuresOf } from '../measures.js';
12
13
  const REACH = 24;
13
14
  const COEFFICIENTS = new Map();
14
15
  const coefficientIn = (locale) => {
@@ -87,16 +88,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
87
88
  return depth < 0 ? undefined : seriesIndex(depth);
88
89
  }
89
90
  get measures() {
90
- const rows = this.sourceRows;
91
- const key = seriesKey(this.sizeBy);
92
- const size = key === ''
93
- ? undefined
94
- : this.plottedColumns.find((column) => seriesKey(rows.find((row) => row.cells[column])?.cells[column]?.name ?? '') === key);
95
- const [across, ...ups] = this.plottedColumns.filter((column) => column !== size);
96
- return { across, size, ups };
97
- }
98
- showsColumn(cell) {
99
- return cell !== this.measures.size && super.showsColumn(cell);
91
+ return measuresOf(this.plottedColumns, (column) => this.columnName(column), this.sizeBy);
100
92
  }
101
93
  get rulesX() {
102
94
  return this.gridlines !== undefined && this.gridlines !== 'y';
@@ -107,7 +99,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
107
99
  get field() {
108
100
  const rows = this.sourceRows;
109
101
  const { across, size, ups } = this.measures;
110
- const name = (column) => column === undefined ? '' : (rows.find((row) => row.cells[column])?.cells[column]?.name ?? '');
102
+ const name = (column) => column === undefined ? '' : this.columnName(column);
111
103
  const pairs = ups.map((column) => rows.flatMap((row, index) => {
112
104
  const x = across === undefined ? undefined : row.cells[across]?.value;
113
105
  const y = row.cells[column]?.value;
@@ -119,19 +111,14 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
119
111
  const y = nice(extent(all.length > 0 ? all.map(([, one]) => one) : [0]));
120
112
  const along = linear(x, [0, PLOT_WIDTH]);
121
113
  const up = linear(y, [PLOT_HEIGHT, 0]);
122
- const sizes = rows.map((row) => (size === undefined ? undefined : row.cells[size]?.value));
123
- const largest = Math.max(0, ...sizes.map((one) => one ?? 0));
124
- const weight = (row) => {
125
- const one = sizes[row];
126
- return one === undefined || largest <= 0 ? undefined : Math.sqrt(Math.max(0, one) / largest);
127
- };
114
+ const weights = bubbleWeights(rows.map((row) => (size === undefined ? undefined : row.cells[size]?.value)));
128
115
  return {
129
116
  pairs: pairs.map((series) => series.map(([one, two]) => [one, two])),
130
117
  points: pairs.flatMap((series, depth) => shown[depth]
131
118
  ? series.map(([one, two, row]) => ({
132
119
  depth,
133
120
  row,
134
- weight: weight(row),
121
+ weight: weights[row],
135
122
  x: along(one),
136
123
  y: up(two),
137
124
  }))
@@ -21,13 +21,36 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
21
21
  * Set `valueContent` to draw the trigger's content from the chosen options,
22
22
  * and several chosen options are written as a list in the reader's language.
23
23
  *
24
- * In the open list the arrow keys, Home, End, PageUp, PageDown and typed
25
- * letters move the highlight, and Enter, Space or a click chooses it and
26
- * closes the list. Tab closes the list and moves on. On the closed trigger,
27
- * ArrowDown, Enter and Space open the list on the chosen option, ArrowUp opens
28
- * it on the chosen or last option, Home and End open it on the first or last,
29
- * and typed letters choose the option whose words start with them, as a native
30
- * `<select>` does.
24
+ * **Keyboard.** On the closed trigger:
25
+ *
26
+ * | Key | What it does |
27
+ * |---|---|
28
+ * | Down, Enter, Space | Opens the list on the chosen option, or the first. |
29
+ * | Up | Opens the list on the chosen option, or the last. |
30
+ * | Home, End | Opens the list on the first or the last option. |
31
+ * | Left, Right | Chooses the option before or after the chosen one without opening, as React Aria's Select does, in either direction of text. |
32
+ * | A letter or a word | Chooses the option whose words start with what you typed, as a native `<select>` does. With `multiple`, opens the list on it. |
33
+ *
34
+ * In the open list, which is a `Listbox`:
35
+ *
36
+ * | Key | What it does |
37
+ * |---|---|
38
+ * | Down, Up, Home, End, Page Down, Page Up | Moves the highlight. |
39
+ * | A letter or a word | Moves the highlight to the next option whose words start with what you typed. |
40
+ * | Enter, Space | Chooses the highlighted option and closes the list. With `multiple`, toggles it and leaves the list open. |
41
+ * | Escape | Closes the list and puts focus back on the trigger. |
42
+ * | Tab | Closes the list and moves on. |
43
+ *
44
+ * A choice made on the closed trigger is said aloud on Apple devices, where
45
+ * VoiceOver does not read the trigger's new name.
46
+ *
47
+ * **ARIA.** The trigger is a `button` with `aria-haspopup="listbox"` and
48
+ * `aria-expanded`, named by the label and then the chosen words, as a native
49
+ * `<select>` is. The list is the listbox `Listbox` describes, named by the
50
+ * label, or by `list-label` where there is none.
51
+ *
52
+ * Set `autocomplete` to let a browser's autofill choose, as a native `<select>`
53
+ * with the same attribute would.
31
54
  *
32
55
  * @since 0.1.0
33
56
  * @status stable
@@ -69,6 +92,7 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
69
92
  * @csspart listbox-loading - The text shown while the options load.
70
93
  * @csspart listbox-search - The region that holds the filter field.
71
94
  * @csspart listbox-search-field - The filter field.
95
+ * @csspart live - The region a screen reader announces a choice made on the closed trigger from, drawn once there is one to say. Visually hidden in the styled tier.
72
96
  * @csspart surface - The floating panel.
73
97
  * @csspart trigger - The closed control, which shows the chosen value.
74
98
  * @csspart value - The chosen option's text inside the trigger.
@@ -81,6 +105,18 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
81
105
  */
82
106
  export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
83
107
  protected readonly aria: AriaForwardController;
108
+ /** The region a choice made on the closed trigger is said from. */
109
+ private readonly status;
110
+ /**
111
+ * What a browser's autofill may fill in, as a platform token such as
112
+ * `country` or `cc-exp-month`, as on a native `<select>`.
113
+ *
114
+ * Set, the select keeps a hidden native `<select>` with the same options for
115
+ * the browser to fill, as React Aria's `HiddenSelect` does, and takes the
116
+ * option the browser picks, by its value or its words, as the value. Empty
117
+ * draws no hidden select, and a browser fills nothing.
118
+ */
119
+ autocomplete: string;
84
120
  /**
85
121
  * The text shown when no options match the filter.
86
122
  */
@@ -283,9 +319,21 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
283
319
  * Aria's Select do, so a reader who knows the answer types it. A Space in the
284
320
  * middle of those letters is one of them. With `multiple`, a letter opens the
285
321
  * list on the match instead, because adding to a set is a choice the reader
286
- * makes in the list.
322
+ * makes in the list. ArrowLeft and ArrowRight choose the option before and
323
+ * after the chosen one.
287
324
  */
288
325
  private readonly handleTriggerKeydown;
326
+ /**
327
+ * Chooses from the closed trigger, and says the choice on Apple devices.
328
+ *
329
+ * The trigger is named by the label and the chosen words, so a choice made
330
+ * on it renames the control in focus. VoiceOver does not read a name that
331
+ * changes under focus, so a reader there heard nothing as a letter or an
332
+ * arrow changed the answer. The words are said in a live region, as React
333
+ * Aria says a combobox's choice for VoiceOver alone, since other screen
334
+ * readers read the new name.
335
+ */
336
+ private choose;
289
337
  /**
290
338
  * Tab closes the list and lets focus move on, as a native `<select>` does.
291
339
  *