@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
@@ -4,10 +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 { customElement, HelixElement } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, rgbOf } 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';
11
+ import { qrSvg } from '../export.js';
12
+ const QUIET = 4;
11
13
  const isFinder = (x, y, size) => {
12
14
  const near = (a, b) => a < 8 && b < 8;
13
15
  return near(x, y) || near(size - 1 - x, y) || near(x, size - 1 - y);
@@ -104,6 +106,51 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
104
106
  this.overflowed = '';
105
107
  }
106
108
  }
109
+ toSvg(size = 512) {
110
+ const code = this.renderRoot?.querySelector('svg[part="code"]');
111
+ const modules = code?.querySelector('[part="modules"]');
112
+ const finders = code?.querySelector('[part="finders"]');
113
+ if (!code || !modules || !finders) {
114
+ return '';
115
+ }
116
+ const ground = getComputedStyle(this).backgroundColor;
117
+ const clear = ground === '' || ground === 'transparent' || /^rgba\(.*,\s*0\)$/.test(ground);
118
+ return qrSvg({
119
+ background: clear ? '#ffffff' : rgbOf(ground),
120
+ box: Number(code.getAttribute('viewBox')?.split(' ')[2] ?? 0),
121
+ finderColor: rgbOf(getComputedStyle(finders).fill || '#000000'),
122
+ finders: finders.getAttribute('d') ?? '',
123
+ moduleColor: rgbOf(getComputedStyle(modules).fill || '#000000'),
124
+ modules: modules.getAttribute('d') ?? '',
125
+ quiet: QUIET,
126
+ }, size);
127
+ }
128
+ async toPng(size = 512) {
129
+ const markup = this.toSvg(size);
130
+ if (markup === '') {
131
+ return null;
132
+ }
133
+ const image = new Image();
134
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
135
+ await image.decode();
136
+ const canvas = document.createElement('canvas');
137
+ canvas.width = size;
138
+ canvas.height = size;
139
+ canvas.getContext('2d')?.drawImage(image, 0, 0, size, size);
140
+ return new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
141
+ }
142
+ async download(filename = 'qr-code.png', size = 512) {
143
+ const png = await this.toPng(size);
144
+ if (!png) {
145
+ return;
146
+ }
147
+ const url = URL.createObjectURL(png);
148
+ const link = document.createElement('a');
149
+ link.href = url;
150
+ link.download = filename;
151
+ link.click();
152
+ setTimeout(() => URL.revokeObjectURL(url));
153
+ }
107
154
  renderCode() {
108
155
  if (this.value === '') {
109
156
  return nothing;
@@ -113,7 +160,7 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
113
160
  return nothing;
114
161
  }
115
162
  const size = result.size;
116
- const quiet = 4;
163
+ const quiet = QUIET;
117
164
  const box = size + quiet * 2;
118
165
  const finders = pathFor(result.data, 'square', 'finders');
119
166
  return svg `<svg
@@ -23,6 +23,13 @@ declare const HelixHeadlessRadioGroup_base: (new (...args: any[]) => import("@fu
23
23
  * the value until something is chosen, and where a reset returns. The two work
24
24
  * like `checked` and `defaultChecked`.
25
25
  *
26
+ * A choice sends its events in this order: `helix-select` from the radio, then,
27
+ * a microtask later and only if nobody refused it, `helix-before-change` from
28
+ * the group, and once the value has moved, `input` and `change`. The microtask
29
+ * is what lets a listener you add to the group after it connected refuse the
30
+ * `helix-select`. A native radio sends `input` and `change` in the same task as
31
+ * the click.
32
+ *
26
33
  * @since 0.1.0
27
34
  * @status stable
28
35
  * @aliases RadioButtons, Choice group
@@ -69,6 +76,22 @@ export declare class HelixHeadlessRadioGroup extends HelixHeadlessRadioGroup_bas
69
76
  * only `horizontal` has to be named.
70
77
  */
71
78
  orientation?: 'horizontal' | 'vertical';
79
+ /**
80
+ * Whether a reader can see and focus the answer and cannot change it. A
81
+ * read-only group still submits its value.
82
+ *
83
+ * React Aria's `isReadOnly`. A press on a radio is refused, and the arrow
84
+ * keys, Home and End leave focus on the answer, so Tab still reaches it and
85
+ * a reader hears it with the group's name. The group says `aria-readonly`.
86
+ * A script can still set `value`.
87
+ */
88
+ readOnly: boolean;
89
+ /**
90
+ * Keeps the keys that would move the answer from the keyboard controller
91
+ * while the group is read-only. A capture listener on the host runs ahead of
92
+ * the controller's own on the same target.
93
+ */
94
+ private readonly holdKeys;
72
95
  protected readonly keys: KeyboardNavigationController;
73
96
  protected readonly cascade: DisabledCascadeController;
74
97
  /** The radios, in light-DOM order. Client only: the server has no tree. */
@@ -8,16 +8,28 @@ import { customElement, DisabledCascadeController, HelixFormElement, hostAria, K
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessRadio } from '../root/radio-root.js';
11
+ const MOVING_KEYS = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'End', 'Home']);
11
12
  let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControllerValue(MixinField(HelixFormElement)) {
12
13
  constructor() {
13
14
  super(...arguments);
14
15
  this.focusable = true;
16
+ this.readOnly = false;
17
+ this.holdKeys = (event) => {
18
+ if (this.readOnly && MOVING_KEYS.has(event.key)) {
19
+ event.stopImmediatePropagation();
20
+ event.preventDefault();
21
+ }
22
+ };
15
23
  this.keys = new KeyboardNavigationController(this, { items: () => this.items });
16
24
  this.cascade = new DisabledCascadeController(this, {
17
25
  items: () => this.items,
18
26
  });
19
27
  this.handleSelection = (event) => {
20
28
  event.stopPropagation();
29
+ if (this.readOnly) {
30
+ event.preventDefault();
31
+ return;
32
+ }
21
33
  queueMicrotask(() => {
22
34
  if (event.defaultPrevented) {
23
35
  return;
@@ -80,16 +92,19 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
80
92
  connectedCallback() {
81
93
  super.connectedCallback();
82
94
  this.addEventListener('helix-select', this.handleSelection);
95
+ this.addEventListener('keydown', this.holdKeys, { capture: true });
83
96
  }
84
97
  disconnectedCallback() {
85
98
  super.disconnectedCallback();
86
99
  this.removeEventListener('helix-select', this.handleSelection);
100
+ this.removeEventListener('keydown', this.holdKeys, { capture: true });
87
101
  }
88
102
  willUpdate(changed) {
89
103
  super.willUpdate(changed);
90
104
  this.adopt();
91
105
  hostAria(this.internals, { label: this.label, role: 'radiogroup' });
92
106
  this.internals.ariaRequired = this.required ? 'true' : null;
107
+ this.internals.ariaReadOnly = this.readOnly ? 'true' : null;
93
108
  }
94
109
  updated(changed) {
95
110
  super.updated(changed);
@@ -111,6 +126,9 @@ __decorate([
111
126
  __decorate([
112
127
  property({ reflect: true })
113
128
  ], HelixHeadlessRadioGroup.prototype, "orientation", void 0);
129
+ __decorate([
130
+ property({ attribute: 'readonly', reflect: true, type: Boolean })
131
+ ], HelixHeadlessRadioGroup.prototype, "readOnly", void 0);
114
132
  HelixHeadlessRadioGroup = __decorate([
115
133
  customElement('helix-headless-radio-group')
116
134
  ], HelixHeadlessRadioGroup);
@@ -15,6 +15,9 @@ declare const HelixHeadlessRating_base: (new (...args: any[]) => import("@fusion
15
15
  * A read-only rating displays a score, which is the most common use. It is
16
16
  * announced as a displayed value and does not look disabled.
17
17
  *
18
+ * With `clearable`, pressing the chosen score again, or Backspace or Delete on
19
+ * the marks, takes the rating back to unrated and sends `helix-clear`.
20
+ *
18
21
  * @since 0.1.0
19
22
  * @status stable
20
23
  * @category forms
@@ -28,6 +31,7 @@ declare const HelixHeadlessRating_base: (new (...args: any[]) => import("@fusion
28
31
  *
29
32
  * @event {Event} change - The score was chosen. Sent together with `input`, because each choice commits the score.
30
33
  * @event {HelixBeforeChangeEvent} helix-before-change - The score is about to change. Cancelable: cancel it to keep the current score.
34
+ * @event {Event} helix-clear - The rating went back to unrated, under `clearable`. Sent after `input` and `change`.
31
35
  * @event {Event} input - The score changed.
32
36
  *
33
37
  * @csspart feedback - The message that says what is wrong.
@@ -50,6 +54,12 @@ declare const HelixHeadlessRating_base: (new (...args: any[]) => import("@fusion
50
54
  */
51
55
  export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
52
56
  protected readonly announcesInput = false;
57
+ /**
58
+ * Whether a reader can take the rating back to unrated: by pressing the
59
+ * chosen score again, or with Backspace or Delete. Zag's rating group
60
+ * clears the same way.
61
+ */
62
+ clearable: boolean;
53
63
  /**
54
64
  * How many marks there are.
55
65
  */
@@ -142,6 +152,8 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
142
152
  */
143
153
  valueText: string;
144
154
  protected readonly aria: AriaForwardController;
155
+ /** @internal The element a missing name is checked on: the set of stars, so the name of one star does not count as the name of the rating. */
156
+ protected get namedControl(): HTMLElement | null;
145
157
  /**
146
158
  * The shape a consumer slotted, if they slotted one.
147
159
  *
@@ -236,8 +248,17 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
236
248
  * or off the element entirely, has asked for nothing.
237
249
  */
238
250
  private readonly handleDragStart;
251
+ /** The score a press began on, so releasing on the chosen one can clear it. */
252
+ private pressedOn;
253
+ /**
254
+ * Whether a drag has just chosen, so the click a browser may send after it
255
+ * does not choose again. Cleared a task later, since the click may not come.
256
+ */
257
+ private dragChose;
239
258
  private readonly handleDragMove;
240
259
  private readonly handleDragEnd;
260
+ /** Whether a press on `score` takes a clearable rating back to unrated. */
261
+ private clearsAt;
241
262
  private readonly handleDragCancel;
242
263
  /**
243
264
  * Reports the value, and whether the form may go without one.
@@ -260,6 +281,10 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
260
281
  */
261
282
  private sync;
262
283
  private readonly handleOptionClick;
284
+ /** Backspace and Delete take a clearable rating back to unrated. */
285
+ private readonly handleClearKey;
286
+ /** Back to unrated, through the same cancelable request as a score, and said. */
287
+ private clear;
263
288
  private readonly handleOptionEnter;
264
289
  private readonly handleLeave;
265
290
  connectedCallback(): void;
@@ -11,6 +11,7 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
11
11
  constructor() {
12
12
  super(...arguments);
13
13
  this.announcesInput = false;
14
+ this.clearable = false;
14
15
  this.max = 5;
15
16
  this.min = 0;
16
17
  this.optionLabel = '{score} out of {max}';
@@ -50,8 +51,11 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
50
51
  catch {
51
52
  }
52
53
  this.dragging = event.pointerId;
54
+ this.pressedOn = score;
53
55
  this.previewing = Number.parseFloat(score);
54
56
  };
57
+ this.pressedOn = undefined;
58
+ this.dragChose = false;
55
59
  this.handleDragMove = (event) => {
56
60
  if (this.dragging !== event.pointerId) {
57
61
  return;
@@ -68,9 +72,18 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
68
72
  this.dragging = undefined;
69
73
  this.previewing = undefined;
70
74
  const score = this.scoreAt(event.clientX);
71
- if (score !== undefined) {
72
- this.requestValue(score);
75
+ if (score === undefined) {
76
+ return;
77
+ }
78
+ this.dragChose = true;
79
+ setTimeout(() => {
80
+ this.dragChose = false;
81
+ }, 0);
82
+ if (this.clearsAt(score) && score === this.pressedOn) {
83
+ this.clear();
84
+ return;
73
85
  }
86
+ this.requestValue(score);
74
87
  };
75
88
  this.handleDragCancel = (event) => {
76
89
  if (this.dragging !== event.pointerId) {
@@ -80,8 +93,22 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
80
93
  this.previewing = undefined;
81
94
  };
82
95
  this.handleOptionClick = (event) => {
96
+ if (this.dragChose) {
97
+ return;
98
+ }
83
99
  const option = event.currentTarget;
84
- this.requestValue(option.dataset.score ?? '');
100
+ const score = option.dataset.score ?? '';
101
+ if (this.clearsAt(score)) {
102
+ this.clear();
103
+ return;
104
+ }
105
+ this.requestValue(score);
106
+ };
107
+ this.handleClearKey = (event) => {
108
+ if (this.clearable && (event.key === 'Backspace' || event.key === 'Delete')) {
109
+ event.preventDefault();
110
+ this.clear();
111
+ }
85
112
  };
86
113
  this.handleOptionEnter = (event) => {
87
114
  if (this.isDisabled) {
@@ -95,6 +122,9 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
95
122
  };
96
123
  this.warned = false;
97
124
  }
125
+ get namedControl() {
126
+ return this.partNamed('marks');
127
+ }
98
128
  get floor() {
99
129
  if (!Number.isFinite(this.min)) {
100
130
  return 0;
@@ -156,10 +186,18 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
156
186
  }
157
187
  return nearest;
158
188
  }
189
+ clearsAt(score) {
190
+ return this.clearable && this.value !== '' && Number(score) === Number(this.value);
191
+ }
159
192
  sync() {
160
193
  this.internals.setFormValue(this.value === '' ? null : this.value);
161
194
  this.reportRequired(this.value === '', 'Choose a rating to continue.', 'marks');
162
195
  }
196
+ clear() {
197
+ if (this.value !== '' && !this.readOnly && !this.isDisabled && this.requestValue('')) {
198
+ this.dispatchEvent(new Event('helix-clear', { bubbles: true, composed: true }));
199
+ }
200
+ }
163
201
  connectedCallback() {
164
202
  super.connectedCallback();
165
203
  this.addEventListener('pointerleave', this.handleLeave);
@@ -226,6 +264,7 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
226
264
  data-slotted=${this.markSource ? '' : nothing}
227
265
  style=${`--rating-full: ${Math.floor(showing)}; --rating-part: ${showing - Math.floor(showing)}; --rating-steps: ${this.positions.length}`}
228
266
  @pointercancel=${this.handleDragCancel}
267
+ @keydown=${this.handleClearKey}
229
268
  @pointerdown=${this.handleDragStart}
230
269
  @pointermove=${this.handleDragMove}
231
270
  @pointerup=${this.handleDragEnd}
@@ -283,6 +322,9 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
283
322
  this.value = fromNumber(count);
284
323
  }
285
324
  };
325
+ __decorate([
326
+ property({ type: Boolean })
327
+ ], HelixHeadlessRating.prototype, "clearable", void 0);
286
328
  __decorate([
287
329
  property({ type: Number })
288
330
  ], HelixHeadlessRating.prototype, "max", void 0);
@@ -33,7 +33,14 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
33
33
  * round numbers, and the range extends to them.
34
34
  *
35
35
  * The summary gives each series' correlation coefficient and its number of
36
- * points. The table has every number, and the drawing is `aria-hidden`.
36
+ * points. The table has every number, and the drawing is hidden from screen
37
+ * readers.
38
+ *
39
+ * **The plot is read by key.** It takes focus, named by `label` or the caption.
40
+ * The left and right arrows move from row to row in the table's order, Home
41
+ * and End go to the first and the last, and the up and down arrows move among
42
+ * the series of a row. The tooltip sits on that point, the `live` part says the
43
+ * row in words, and Escape puts the tooltip away.
37
44
  *
38
45
  * Each point is a `<span>` placed by percentage over the drawing, as
39
46
  * `LineChart` places its end points, so points stay round when the drawing
@@ -52,9 +59,10 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
52
59
  * @documentation https://fusion.dev/helix/components/scatter-chart
53
60
  *
54
61
  * @csspart gridline - One line across the plot, drawn when `gridlines` is set.
62
+ * @csspart live - The readout: a polite live region that says the row the keys moved to. Hide it visually.
55
63
  * @csspart mark - The invisible point the tooltip is anchored to.
56
64
  * @csspart plane - The grid that holds the plot, both axes, and their names. It runs left to right on every page.
57
- * @csspart plot - The drawing.
65
+ * @csspart plot - The drawing, which takes focus to be read by key.
58
66
  * @csspart point - One row, as a point.
59
67
  * @csspart point-active - A point in the row under the pointer. Set alongside `point`.
60
68
  * @csspart summary - The text that says how strongly the measures relate, for people who cannot see the points.
@@ -68,6 +76,7 @@ declare const HelixHeadlessScatterChart_base: typeof HelixElement & (new (...arg
68
76
  * @csspart tooltip-arrow-top - The point while the bubble is above the reading, on its bottom edge. Set alongside `tooltip-arrow`.
69
77
  * @csspart tooltip-bottom - The bubble while it is below the reading. Set alongside `tooltip`.
70
78
  * @csspart tooltip-cell - One column's name and value inside the tooltip.
79
+ * @csspart tooltip-cell-current - The cell the up and down arrows rest on, while they rest there. Set alongside `tooltip-cell`.
71
80
  * @csspart tooltip-label - The row's own name inside the tooltip.
72
81
  * @csspart tooltip-left - The bubble while it is left of the reading. Set alongside `tooltip`.
73
82
  * @csspart tooltip-name - A column's name inside the tooltip.
@@ -149,7 +158,12 @@ export declare class HelixHeadlessScatterChart extends HelixHeadlessScatterChart
149
158
  * absent where there are fewer than three points or no spread to measure.
150
159
  */
151
160
  protected renderSummary(field: ScatterField): TemplateResult;
152
- /** The row a pointer is resting on, over the point it is nearest. */
161
+ /**
162
+ * The row a pointer is resting on, over the point it is nearest.
163
+ *
164
+ * A row reached by key has no nearest point, so the bubble sits on the
165
+ * point of the series the keys rest on, or on the row's first point.
166
+ */
153
167
  protected renderTooltip(): TemplateResult | typeof nothing;
154
168
  /**
155
169
  * The point a pointer is nearest, within reach.
@@ -116,14 +116,17 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
116
116
  const y = row.cells[column]?.value;
117
117
  return x === undefined || y === undefined ? [] : [[x, y, index]];
118
118
  }));
119
- const all = pairs.flat();
119
+ const shown = ups.map((column) => this.showsSeries(name(column)));
120
+ const all = pairs.filter((_, depth) => shown[depth]).flat();
120
121
  const x = nice(extent(all.length > 0 ? all.map(([one]) => one) : [0]));
121
122
  const y = nice(extent(all.length > 0 ? all.map(([, one]) => one) : [0]));
122
123
  const along = linear(x, [0, PLOT_WIDTH]);
123
124
  const up = linear(y, [PLOT_HEIGHT, 0]);
124
125
  return {
125
126
  pairs: pairs.map((series) => series.map(([one, two]) => [one, two])),
126
- points: pairs.flatMap((series, depth) => series.map(([one, two, row]) => ({ depth, row, x: along(one), y: up(two) }))),
127
+ points: pairs.flatMap((series, depth) => shown[depth]
128
+ ? series.map(([one, two, row]) => ({ depth, row, x: along(one), y: up(two) }))
129
+ : []),
127
130
  x,
128
131
  xName: name(across),
129
132
  y,
@@ -156,8 +159,17 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
156
159
  }
157
160
  renderPlot(field, along, up) {
158
161
  const at = this.pointer.at;
159
- return html `<div aria-hidden="true" id=${CHART_PLOT} part="plot">
162
+ return html `<div
163
+ aria-describedby=${this.named ? 'helix-chart-summary' : nothing}
164
+ aria-label=${this.named || nothing}
165
+ aria-labelledby=${this.named ? nothing : 'helix-chart-summary'}
166
+ id=${CHART_PLOT}
167
+ part="plot"
168
+ role="application"
169
+ tabindex="0"
170
+ >
160
171
  <svg part="svg"
172
+ aria-hidden="true"
161
173
  preserveAspectRatio="none"
162
174
  viewBox="0 0 ${PLOT_WIDTH} ${PLOT_HEIGHT}"
163
175
  xmlns="http://www.w3.org/2000/svg"
@@ -172,6 +184,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
172
184
  ${field.points.map((point) => {
173
185
  const index = seriesIndex(point.depth);
174
186
  return html `<span
187
+ aria-hidden="true"
175
188
  data-series=${index}
176
189
  ?data-active=${point.row === at}
177
190
  part="point${point.row === at ? ' point-active' : ''}"
@@ -189,13 +202,18 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
189
202
  return `${name} / ${field.xName}: ${pairs.length} ${this.pointsLabel}${strength}`;
190
203
  });
191
204
  const named = this.named ? `${this.named}. ` : '';
192
- return html `<p part="summary">${named}${clauses.join('; ')}.</p>`;
205
+ return html `<p id="helix-chart-summary" part="summary">${named}${clauses.join('; ')}.</p>`;
193
206
  }
194
207
  renderTooltip() {
195
208
  const at = this.pointer.at;
196
- const point = this.nearest;
209
+ const column = this.pointer.column;
210
+ const depth = column === undefined ? -1 : this.plottedColumns.indexOf(column) - 1;
211
+ const point = this.nearest?.row === at && !this.pointer.keyed
212
+ ? this.nearest
213
+ : (this.drawn.find((one) => one.row === at && one.depth === depth) ??
214
+ this.drawn.find((one) => one.row === at));
197
215
  if (at < 0 || !point) {
198
- return drawTooltip(this.sourceRows, -1, this.tag('tooltip'));
216
+ return drawTooltip(this.sourceRows, -1, this.tag('tooltip'), undefined, undefined, this.pointer);
199
217
  }
200
218
  return drawTooltip(this.sourceRows, at, this.tag('tooltip'), {
201
219
  dir: isRtl(this) ? 'rtl' : 'ltr',
@@ -203,7 +221,7 @@ let HelixHeadlessScatterChart = class HelixHeadlessScatterChart extends MixinCha
203
221
  placement: 'top',
204
222
  x: `${point.x}%`,
205
223
  y: `${point.y}%`,
206
- }, (cell) => this.seriesOf(cell));
224
+ }, (cell) => this.seriesOf(cell), this.pointer);
207
225
  }
208
226
  };
209
227
  __decorate([
@@ -1,5 +1,6 @@
1
1
  import { AriaForwardController, HelixFormElement, PopoverController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import { type HelixHeadlessOption } from '../../option/root/option-root.js';
3
4
  import '../../listbox/index.js';
4
5
  declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
5
6
  /**
@@ -17,6 +18,9 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
17
18
  * `Select` owns the value, passes it down to the listbox, and takes changes
18
19
  * back, so the value lives in one place.
19
20
  *
21
+ * Set `valueContent` to draw the trigger's content from the chosen options,
22
+ * and several chosen options are written as a list in the reader's language.
23
+ *
20
24
  * In the open list the arrow keys, Home, End, PageUp, PageDown and typed
21
25
  * letters move the highlight, and Enter, Space or a click chooses it and
22
26
  * closes the list. Tab closes the list and moves on. On the closed trigger,
@@ -103,6 +107,17 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
103
107
  * Shown on the trigger while nothing is chosen.
104
108
  */
105
109
  placeholder: string;
110
+ /**
111
+ * What the trigger shows for the chosen options: a function given them, in
112
+ * the order they appear, that returns text, an element or a Lit template.
113
+ *
114
+ * For a trigger that shows more than the words, an avatar beside a name or a
115
+ * count of a long set, as React Aria's `SelectValue` takes children. The
116
+ * trigger is named by what this draws, so keep the words in it. Nothing
117
+ * chosen still shows `placeholder`. A property only, there being no way to
118
+ * write a function in markup.
119
+ */
120
+ valueContent?: ((chosen: readonly HelixHeadlessOption[]) => unknown) | undefined;
106
121
  /**
107
122
  * Which option is chosen, as an index, or `-1` where none is.
108
123
  *
@@ -168,6 +183,13 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
168
183
  * the commit creates the text node it needs.
169
184
  */
170
185
  private get display();
186
+ /** The chosen options, in the order they appear. None on a server or while hydrating. */
187
+ private get chosenOptions();
188
+ /**
189
+ * What the trigger shows, behind a seam: what `value-content` draws for the
190
+ * chosen options, or their words, or the placeholder.
191
+ */
192
+ protected renderValue(): unknown;
171
193
  /** Where the highlight goes when the list opens, set by the key that opened it. */
172
194
  private opening;
173
195
  /**
@@ -187,6 +209,8 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
187
209
  * Made on connection, there being no `MutationObserver` on a server.
188
210
  */
189
211
  private watchers;
212
+ /** A link option, once followed, closes the panel as a choice does. */
213
+ private readonly handleLinkFollowed;
190
214
  connectedCallback(): void;
191
215
  disconnectedCallback(): void;
192
216
  /** Opens the list with the highlight where the opening key says. */
@@ -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 { AriaForwardController, customElement, HelixFormElement, hostAria, MixinControllerOpen, MixinControllerValue, MixinField, PopoverController, reportValues, typesCharacter, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, afterLinkFollowed, customElement, HelixFormElement, hostAria, MixinControllerOpen, MixinControllerValue, MixinField, PopoverController, reportValues, typesCharacter, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -43,6 +43,9 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
43
43
  this.opening = 'selected';
44
44
  this.seen = new Set();
45
45
  this.watchers = [];
46
+ this.handleLinkFollowed = (event) => {
47
+ afterLinkFollowed(event, () => this.requestOpen(false));
48
+ };
46
49
  this.handleChange = (event) => {
47
50
  const next = event.detail.value;
48
51
  if (!this.requestValue(next)) {
@@ -105,16 +108,23 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
105
108
  }
106
109
  get display() {
107
110
  const chosen = new Set(this.value === '' ? [] : this.value.split(','));
108
- const words = optionsAmong(this.knownChildren)
109
- .filter((option) => chosen.has(option.value))
110
- .map((option) => option.text);
111
+ const words = this.chosenOptions.map((option) => option.text);
111
112
  if (words.length > 0) {
112
- return words.join(', ');
113
+ return new Intl.ListFormat(this.lang || undefined, { style: 'short', type: 'unit' }).format(words);
113
114
  }
114
115
  return chosen.size > 0 && !this.childrenKnown ? nothing : this.placeholder;
115
116
  }
117
+ get chosenOptions() {
118
+ const chosen = new Set(this.value === '' ? [] : this.value.split(','));
119
+ return optionsAmong(this.knownChildren).filter((option) => chosen.has(option.value));
120
+ }
121
+ renderValue() {
122
+ const chosen = this.chosenOptions;
123
+ return this.valueContent && chosen.length > 0 ? this.valueContent(chosen) : this.display;
124
+ }
116
125
  connectedCallback() {
117
126
  super.connectedCallback();
127
+ this.addEventListener('helix-select', this.handleLinkFollowed);
118
128
  const words = new MutationObserver(() => this.requestUpdate());
119
129
  words.observe(this, {
120
130
  attributeFilter: ['label', 'value'],
@@ -128,6 +138,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
128
138
  }
129
139
  disconnectedCallback() {
130
140
  super.disconnectedCallback();
141
+ this.removeEventListener('helix-select', this.handleLinkFollowed);
131
142
  for (const watcher of this.watchers) {
132
143
  watcher.disconnect();
133
144
  }
@@ -252,7 +263,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
252
263
  @keydown=${this.handleTriggerKeydown}
253
264
  >
254
265
  <slot name="start"></slot>
255
- <span part="value" id="value">${this.display}</span>
266
+ <span part="value" id="value">${this.renderValue()}</span>
256
267
  ${''}
257
268
  <slot name="end"></slot>
258
269
  ${''}
@@ -283,6 +294,9 @@ __decorate([
283
294
  __decorate([
284
295
  property()
285
296
  ], HelixHeadlessSelect.prototype, "placeholder", void 0);
297
+ __decorate([
298
+ property({ attribute: false })
299
+ ], HelixHeadlessSelect.prototype, "valueContent", void 0);
286
300
  HelixHeadlessSelect = __decorate([
287
301
  customElement('helix-headless-select')
288
302
  ], HelixHeadlessSelect);
@@ -15,12 +15,22 @@ import { type PropertyValues, type TemplateResult } from 'lit';
15
15
  * There is no `shape` property. Whether a placeholder is a line, a block, or a
16
16
  * circle is presentation, which belongs to the styled tier.
17
17
  *
18
+ * **Put the real content inside and set `loading`, and the skeleton takes its
19
+ * size.** While `loading` is set the content is inert, the host is
20
+ * `aria-busy`, and the styled tier hides the content and paints the
21
+ * placeholder over its box, so nothing moves when the content arrives. Take
22
+ * `loading` off and the content is shown as it is. An empty skeleton is a
23
+ * placeholder of its own size, as before.
24
+ *
18
25
  * @since 0.1.0
19
26
  * @status stable
20
27
  * @category feedback
21
28
  * @aliases Placeholder, Shimmer, Ghost
22
29
  * @documentation https://fusion.dev/helix/components/skeleton
23
30
  *
31
+ * @slot - The content the skeleton stands in for, which sizes it. Optional.
32
+ *
33
+ * @csspart content - The box around the content, inert while `loading`.
24
34
  * @csspart skeleton - The placeholder shape.
25
35
  */
26
36
  export declare class HelixHeadlessSkeleton extends HelixElement {
@@ -31,7 +41,22 @@ export declare class HelixHeadlessSkeleton extends HelixElement {
31
41
  * Absent hides the placeholder from screen readers.
32
42
  */
33
43
  label: string;
44
+ /**
45
+ * Whether the content inside is still loading, which keeps it inert and
46
+ * draws the placeholder over it.
47
+ *
48
+ * Absent shows the content. An empty skeleton is a placeholder whatever
49
+ * this says. Reflected, because the styled tier hides the content on it.
50
+ */
51
+ loading: boolean;
52
+ /**
53
+ * Whether the skeleton holds content, read off its children where they can
54
+ * be known. Not on a server, where an unlabeled skeleton is taken for an
55
+ * empty one until the page has its children.
56
+ */
57
+ private get wraps();
34
58
  protected willUpdate(changed: PropertyValues): void;
59
+ protected firstUpdated(changed: PropertyValues): void;
35
60
  protected render(): TemplateResult;
36
61
  }
37
62
  declare global {