@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
@@ -17,6 +17,9 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
17
17
  this.at = '';
18
18
  this.defaultRead = false;
19
19
  this.locale = '';
20
+ this.position = 0;
21
+ this.setSize = 0;
22
+ this.stop = true;
20
23
  this.unreadLabel = 'Unread';
21
24
  this.isNotification = true;
22
25
  this.readState = undefined;
@@ -58,6 +61,8 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
58
61
  willUpdate(changed) {
59
62
  super.willUpdate(changed);
60
63
  hostAria(this.internals, { role: 'listitem' });
64
+ this.internals.ariaPosInSet = this.position > 0 ? String(this.position) : null;
65
+ this.internals.ariaSetSize = this.setSize === 0 ? null : String(this.setSize);
61
66
  this.toggleState('read', this.read);
62
67
  }
63
68
  updated(changed) {
@@ -105,10 +110,15 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends MixinDis
105
110
  ></${stamp}>`;
106
111
  }
107
112
  render() {
108
- return html `<div part="item">
113
+ return html `<div
114
+ part="item"
115
+ role="group"
116
+ aria-labelledby="label"
117
+ tabindex=${this.stop ? 0 : -1}
118
+ >
109
119
  <span part="media"><slot name="media"></slot></span>
110
120
  <div part="body">
111
- <div part="label">${this.read ? nothing : html `<span role="img" aria-label=${this.unreadLabel}></span>`}<slot></slot></div>
121
+ <div id="label" part="label">${this.read ? nothing : html `<span role="img" aria-label=${this.unreadLabel}></span>`}<slot></slot></div>
112
122
  <slot name="description"></slot>
113
123
  <slot name="detail"></slot>
114
124
  <span part="actions"><slot name="actions" @slotchange=${this.handleActions}></slot></span>
@@ -152,9 +162,18 @@ __decorate([
152
162
  __decorate([
153
163
  property()
154
164
  ], HelixHeadlessNotification.prototype, "locale", void 0);
165
+ __decorate([
166
+ property({ attribute: false })
167
+ ], HelixHeadlessNotification.prototype, "position", void 0);
155
168
  __decorate([
156
169
  property({ type: Boolean })
157
170
  ], HelixHeadlessNotification.prototype, "read", null);
171
+ __decorate([
172
+ property({ attribute: false })
173
+ ], HelixHeadlessNotification.prototype, "setSize", void 0);
174
+ __decorate([
175
+ property({ attribute: false })
176
+ ], HelixHeadlessNotification.prototype, "stop", void 0);
158
177
  __decorate([
159
178
  property({ attribute: 'unread-label' })
160
179
  ], HelixHeadlessNotification.prototype, "unreadLabel", void 0);
@@ -21,6 +21,14 @@ declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("
21
21
  * the items and written to an attribute, so a stylesheet and a badge can both
22
22
  * use it without counting.
23
23
  *
24
+ * **The feed is one `Tab` stop for its rows.** Each row takes focus itself and
25
+ * is read by its label. `ArrowDown` and `ArrowUp` move to the next and the
26
+ * previous row, `Home` and `End` to the first and the last, and `PageDown` and
27
+ * `PageUp` move a row on or back from anywhere inside one, as in the APG feed
28
+ * pattern. The controls inside a row stay in the `Tab` order. Set `total` when
29
+ * the feed holds one page of a longer list, and each row says its place in the
30
+ * whole of it.
31
+ *
24
32
  * @since 0.1.0
25
33
  * @status stable
26
34
  * @category feedback
@@ -67,6 +75,14 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
67
75
  * Label for the mark-all control.
68
76
  */
69
77
  markAllLabel: string;
78
+ /**
79
+ * How many notifications there are in all, where the feed holds only some of
80
+ * them, read as each row's `aria-setsize`. `-1` says the count is unknown.
81
+ *
82
+ * Unset, the browser counts the rows it can see, which is right for a feed
83
+ * that holds everything.
84
+ */
85
+ total?: number;
70
86
  /**
71
87
  * How many items are unread, counted from the items themselves.
72
88
  *
@@ -158,6 +174,32 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
158
174
  */
159
175
  private readonly handleRead;
160
176
  private assign;
177
+ /** The row `Tab` stops on, once a reader has been to one. */
178
+ private stopRow;
179
+ /**
180
+ * The rows a reader can reach with the keys: every row in the feed, in order,
181
+ * less the ones a folded group renders nowhere.
182
+ */
183
+ private get reachable();
184
+ /** The row whose own box is the element focus is on, if it is one. */
185
+ private rowFocused;
186
+ /** A row taking focus becomes the feed's stop, so `Tab` comes back to it. */
187
+ private readonly handleFocusIn;
188
+ /**
189
+ * Moves between rows.
190
+ *
191
+ * The arrows and `Home` and `End` only from a row's own box, so a control in
192
+ * a row keeps its keys. `PageDown` and `PageUp` from anywhere in a row, which
193
+ * is how the APG feed pattern moves on from inside an article.
194
+ */
195
+ private readonly handleKeydown;
196
+ /**
197
+ * Tells each row whether it is the stop, and where it sits in the feed.
198
+ *
199
+ * Through each row's own properties, written once it has hydrated, so a
200
+ * server's markup stays the server's: every row is a stop until this runs.
201
+ */
202
+ private rove;
161
203
  connectedCallback(): void;
162
204
  disconnectedCallback(): void;
163
205
  protected willUpdate(changed: PropertyValues): void;
@@ -4,9 +4,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, focusableElements, HelixElement, HelixReadEvent, hostAria, MixinCollection, markRegion, } from '@fusion.dev/helix-core';
7
+ import { customElement, deepContains, focusableElements, HelixElement, HelixReadEvent, hostAria, MixinCollection, markRegion, rovingStop, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { feedStep } from '../feed-keys.js';
10
11
  let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinCollection(HelixElement) {
11
12
  constructor() {
12
13
  super(...arguments);
@@ -20,6 +21,36 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
20
21
  this.handleRead = () => {
21
22
  queueMicrotask(() => this.requestUpdate());
22
23
  };
24
+ this.handleFocusIn = (event) => {
25
+ const row = this.rowFocused(event.composedPath()[0]);
26
+ if (row && row !== this.stopRow) {
27
+ this.stopRow = row;
28
+ this.rove();
29
+ }
30
+ };
31
+ this.handleKeydown = (event) => {
32
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) {
33
+ return;
34
+ }
35
+ const rows = this.reachable;
36
+ const from = event.composedPath()[0];
37
+ const own = this.rowFocused(from);
38
+ const inside = own ?? rows.find((row) => from instanceof Node && deepContains(row, from));
39
+ if (!inside) {
40
+ return;
41
+ }
42
+ const to = feedStep(event.key, rows.indexOf(inside), rows.length, own !== undefined);
43
+ if (to === undefined) {
44
+ return;
45
+ }
46
+ event.preventDefault();
47
+ const next = rows[to];
48
+ if (next) {
49
+ this.stopRow = next;
50
+ this.rove();
51
+ rowBox(next)?.focus();
52
+ }
53
+ };
23
54
  this.handleMarkAll = (event) => {
24
55
  const button = event.currentTarget;
25
56
  const focused = this.shadowRoot?.activeElement === button;
@@ -106,13 +137,36 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
106
137
  slot?.assign?.(...[...this.children].filter((child) => child.getAttribute('slot') === name));
107
138
  }
108
139
  }
140
+ get reachable() {
141
+ return this.items.filter((row) => !('assignedSlot' in row) || row.assignedSlot !== null);
142
+ }
143
+ rowFocused(target) {
144
+ return this.reachable.find((row) => rowBox(row) === target);
145
+ }
146
+ rove() {
147
+ const rows = this.reachable;
148
+ const stop = rovingStop(rows, this.stopRow);
149
+ const direct = this.rows;
150
+ for (const row of this.items) {
151
+ const position = direct.indexOf(row) + 1;
152
+ whenHydrated(row, () => {
153
+ row.stop = row === stop;
154
+ row.position = this.total === undefined ? 0 : position;
155
+ row.setSize = this.total ?? 0;
156
+ });
157
+ }
158
+ }
109
159
  connectedCallback() {
110
160
  super.connectedCallback();
111
161
  this.addEventListener('helix-read', this.handleRead);
162
+ this.addEventListener('focusin', this.handleFocusIn);
163
+ this.addEventListener('keydown', this.handleKeydown);
112
164
  }
113
165
  disconnectedCallback() {
114
166
  super.disconnectedCallback();
115
167
  this.removeEventListener('helix-read', this.handleRead);
168
+ this.removeEventListener('focusin', this.handleFocusIn);
169
+ this.removeEventListener('keydown', this.handleKeydown);
116
170
  }
117
171
  willUpdate(changed) {
118
172
  super.willUpdate(changed);
@@ -130,6 +184,7 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
130
184
  this.assign();
131
185
  this.markEmpty();
132
186
  this.markMedia();
187
+ this.rove();
133
188
  const unread = this.unread > 0 ? String(this.unread) : null;
134
189
  if (this.getAttribute('unread') !== unread) {
135
190
  if (unread === null) {
@@ -167,7 +222,11 @@ __decorate([
167
222
  __decorate([
168
223
  property({ attribute: 'mark-all-label' })
169
224
  ], HelixHeadlessNotifications.prototype, "markAllLabel", void 0);
225
+ __decorate([
226
+ property({ type: Number })
227
+ ], HelixHeadlessNotifications.prototype, "total", void 0);
170
228
  HelixHeadlessNotifications = __decorate([
171
229
  customElement('helix-headless-notifications')
172
230
  ], HelixHeadlessNotifications);
173
231
  export { HelixHeadlessNotifications };
232
+ const rowBox = (row) => row.shadowRoot?.querySelector('[part="item"]') ?? null;
@@ -18,6 +18,17 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
18
18
  *
19
19
  * `inputmode="decimal"` keeps the numeric keypad on phones.
20
20
  *
21
+ * `format-options` shows the number with `Intl.NumberFormat` in `locale`, as a
22
+ * currency, a percent or a unit, while the field is not being edited. Focus
23
+ * shows the number itself, and a typed `$1,200` or `25%` is read back with the
24
+ * same options. The value stays the plain number: `1200`, or `0.25` for 25%.
25
+ * The locale's group separators are read in every field, so `1,234` is a
26
+ * number in `en-US`.
27
+ *
28
+ * On iOS and iPadOS the field is a text field described as a number field,
29
+ * with no `role="spinbutton"`, which VoiceOver reads as a slider you cannot
30
+ * type in. React Aria's NumberField does the same.
31
+ *
21
32
  * Validity follows the platform's rules: `min`, `max`, and `step` are checked
22
33
  * and reported through the host's own `setValidity`, under the platform's flag
23
34
  * names.
@@ -80,6 +91,16 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
80
91
  * The name of the step down button, read by screen readers.
81
92
  */
82
93
  decrementLabel: string;
94
+ /**
95
+ * `Intl.NumberFormat` options the number is shown with while the field is
96
+ * not being edited: a currency, a percent, a unit, or a number of decimals.
97
+ * Written as JSON in the attribute.
98
+ *
99
+ * ```html
100
+ * <helix-number-input format-options='{"style":"currency","currency":"EUR"}'></helix-number-input>
101
+ * ```
102
+ */
103
+ formatOptions?: Intl.NumberFormatOptions;
83
104
  /**
84
105
  * The name of the step up button, read by screen readers.
85
106
  */
@@ -133,6 +154,11 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
133
154
  * Prevents editing. The value is still submitted with the form.
134
155
  */
135
156
  readOnly: boolean;
157
+ /**
158
+ * What the field is described as on iOS and iPadOS, where it carries no
159
+ * spinbutton role. See the class description.
160
+ */
161
+ roleDescription: string;
136
162
  /**
137
163
  * Makes the label a grip that changes the value when it is dragged.
138
164
  *
@@ -163,6 +189,27 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
163
189
  * number alone.
164
190
  */
165
191
  valueText: string;
192
+ /**
193
+ * Whether the mouse wheel steps the number while the field has focus, as
194
+ * React Aria's NumberField does unless told not to. Off, the wheel scrolls
195
+ * the page, which is what a reader passing over a form expects.
196
+ */
197
+ wheel: boolean;
198
+ /** Whether the field has focus, so it shows the number itself and not its format. */
199
+ private editing;
200
+ /**
201
+ * What the field shows: the value as written while it is edited, and with
202
+ * `format-options` once it is not.
203
+ */
204
+ private get shown();
205
+ /** Steps on the wheel, where asked, while the field has focus. */
206
+ private readonly handleWheel;
207
+ private readonly handleFocus;
208
+ /**
209
+ * Whether this is an iPhone or an iPad, read once on the client. A server
210
+ * has no navigator and renders the spinbutton.
211
+ */
212
+ private readonly touchApple;
166
213
  private get control();
167
214
  /** The value as a number, or `null` where the field holds nothing usable. */
168
215
  private get numeric();
@@ -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, valueAsNumber as asNumber, customElement, fillTemplate, numberAsValue as fromNumber, HelixFormElement, holdCaret, MixinControllerValue, MixinField, restoreValue, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, appleTouch, valueAsNumber as asNumber, customElement, fillTemplate, formatNumber, numberAsValue as fromNumber, HelixFormElement, holdCaret, MixinControllerValue, MixinField, parseNumber, restoreValue, } from '@fusion.dev/helix-core';
8
8
  import { minus, plus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
- import { property } from 'lit/decorators.js';
10
+ import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  const SCRUB_PITCH = 4;
13
13
  const SCRUB_FINE = 16;
@@ -24,9 +24,28 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
24
24
  this.locale = '';
25
25
  this.placeholder = '';
26
26
  this.readOnly = false;
27
+ this.roleDescription = 'number field';
27
28
  this.scrub = false;
28
29
  this.step = 1;
29
30
  this.valueText = '';
31
+ this.wheel = false;
32
+ this.editing = false;
33
+ this.handleWheel = (event) => {
34
+ if (!this.wheel || !this.editing || event.deltaY === 0) {
35
+ return;
36
+ }
37
+ event.preventDefault();
38
+ if (event.deltaY < 0) {
39
+ this.stepUp();
40
+ }
41
+ else {
42
+ this.stepDown();
43
+ }
44
+ };
45
+ this.handleFocus = () => {
46
+ this.editing = true;
47
+ };
48
+ this.touchApple = appleTouch(typeof navigator === 'undefined' ? undefined : navigator);
30
49
  this.scrubFrom = 0;
31
50
  this.scrubValue = 0;
32
51
  this.scrubStart = '';
@@ -77,6 +96,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
77
96
  restoreValue(control, this.value);
78
97
  };
79
98
  this.handleBlur = () => {
99
+ this.editing = false;
80
100
  if (this.settle()) {
81
101
  this.announceChange();
82
102
  }
@@ -122,6 +142,13 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
122
142
  }
123
143
  };
124
144
  }
145
+ get shown() {
146
+ const parsed = this.numeric;
147
+ if (this.editing || this.formatOptions === undefined || parsed === null) {
148
+ return this.value;
149
+ }
150
+ return formatNumber(parsed, this.locale, this.formatOptions);
151
+ }
125
152
  get control() {
126
153
  return this.partNamed('control');
127
154
  }
@@ -130,11 +157,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
130
157
  if (text === '') {
131
158
  return null;
132
159
  }
133
- const read = decimalComma(this.locale) && text.includes(',') && !text.includes('.')
134
- ? text.replace(',', '.')
135
- : text;
136
- const parsed = read.includes(',') ? Number.NaN : Number(read);
137
- return Number.isFinite(parsed) ? parsed : null;
160
+ return parseNumber(text, this.locale, this.formatOptions);
138
161
  }
139
162
  onStep(value) {
140
163
  const step = Math.abs(this.step) || 1;
@@ -291,9 +314,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
291
314
  <input
292
315
  part="control"
293
316
  id=${this.controlId}
294
- .value=${this.value}
317
+ .value=${this.shown}
295
318
  type="text"
296
- role="spinbutton"
319
+ role=${this.touchApple ? nothing : 'spinbutton'}
320
+ aria-roledescription=${this.touchApple ? this.roleDescription : nothing}
297
321
  inputmode="decimal"
298
322
  autocomplete=${this.autocomplete || nothing}
299
323
  name=${this.name || nothing}
@@ -303,17 +327,27 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
303
327
  aria-valuemin=${this.min ?? nothing}
304
328
  aria-valuemax=${this.max ?? nothing}
305
329
  aria-valuenow=${parsed ?? nothing}
306
- aria-valuetext=${this.valueText && parsed !== null
307
- ? fillTemplate(this.valueText, { value: this.value })
308
- : nothing}
330
+ aria-valuetext=${parsed === null
331
+ ? nothing
332
+ : this.valueText
333
+ ? fillTemplate(this.valueText, {
334
+ value: this.formatOptions
335
+ ? formatNumber(parsed, this.locale, this.formatOptions)
336
+ : this.value,
337
+ })
338
+ : this.formatOptions
339
+ ? formatNumber(parsed, this.locale, this.formatOptions)
340
+ : nothing}
309
341
  ?disabled=${this.isDisabled}
310
342
  ?readonly=${this.readOnly}
311
343
  ?required=${this.required}
312
344
  @blur=${this.handleBlur}
313
345
  @change=${this.handleChange}
346
+ @focus=${this.handleFocus}
314
347
  @input=${this.handleInput}
315
348
  @keydown=${this.handleKeydown}
316
349
  @keypress=${this.handleKeypress}
350
+ @wheel=${this.handleWheel}
317
351
  />
318
352
  <slot name="end"></slot>
319
353
  <button
@@ -347,6 +381,9 @@ __decorate([
347
381
  __decorate([
348
382
  property({ attribute: 'decrement-label' })
349
383
  ], HelixHeadlessNumberInput.prototype, "decrementLabel", void 0);
384
+ __decorate([
385
+ property({ attribute: 'format-options', type: Object })
386
+ ], HelixHeadlessNumberInput.prototype, "formatOptions", void 0);
350
387
  __decorate([
351
388
  property({ attribute: 'increment-label' })
352
389
  ], HelixHeadlessNumberInput.prototype, "incrementLabel", void 0);
@@ -374,6 +411,9 @@ __decorate([
374
411
  __decorate([
375
412
  property({ attribute: 'readonly', reflect: true, type: Boolean })
376
413
  ], HelixHeadlessNumberInput.prototype, "readOnly", void 0);
414
+ __decorate([
415
+ property({ attribute: 'role-description' })
416
+ ], HelixHeadlessNumberInput.prototype, "roleDescription", void 0);
377
417
  __decorate([
378
418
  property({ reflect: true, type: Boolean })
379
419
  ], HelixHeadlessNumberInput.prototype, "scrub", void 0);
@@ -386,20 +426,16 @@ __decorate([
386
426
  __decorate([
387
427
  property({ attribute: 'value-text' })
388
428
  ], HelixHeadlessNumberInput.prototype, "valueText", void 0);
429
+ __decorate([
430
+ property({ type: Boolean })
431
+ ], HelixHeadlessNumberInput.prototype, "wheel", void 0);
432
+ __decorate([
433
+ state()
434
+ ], HelixHeadlessNumberInput.prototype, "editing", void 0);
389
435
  HelixHeadlessNumberInput = __decorate([
390
436
  customElement('helix-headless-number-input')
391
437
  ], HelixHeadlessNumberInput);
392
438
  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
- };
403
439
  const decimals = (value) => {
404
440
  const text = String(value);
405
441
  const exponent = /e-(\d+)$/.exec(text);
@@ -60,6 +60,7 @@ interface Row {
60
60
  * @csspart boolean - A `true` or `false`.
61
61
  * @csspart copy - The copy button, which copies the active row.
62
62
  * @csspart copy-button - The button inside the copy control.
63
+ * @csspart copy-live - The live region inside the copy button, which says its `pending-label` when the copy becomes pending under focus.
63
64
  * @csspart copy-mark - The clipboard icon, and the confirmation icon that replaces it.
64
65
  * @csspart copy-text - The words on that button, trimmed to their capitals and baseline.
65
66
  * @csspart copy-tooltip - The tooltip, forwarded from the copy button's tooltip.
@@ -353,7 +353,7 @@ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends He
353
353
  renderCopyButton(row) {
354
354
  return staticHtml `<${this.tag('copy-button')}
355
355
  part="copy"
356
- exportparts="button: copy-button, text: copy-text, mark: copy-mark, tooltip: copy-tooltip,
356
+ exportparts="button: copy-button, live: copy-live, text: copy-text, mark: copy-mark, tooltip: copy-tooltip,
357
357
  tooltip-arrow: copy-tooltip-arrow, tooltip-arrow-bottom: copy-tooltip-arrow-bottom,
358
358
  tooltip-arrow-left: copy-tooltip-arrow-left, tooltip-arrow-right: copy-tooltip-arrow-right,
359
359
  tooltip-arrow-top: copy-tooltip-arrow-top, tooltip-bottom: copy-tooltip-bottom,
@@ -1,4 +1,4 @@
1
- import { HelixElement } from '@fusion.dev/helix-core';
1
+ import { HelixElement, type LinkModifiers } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * The options among some elements, reaching one level into a group.
@@ -34,6 +34,9 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
34
34
  * Filtering belongs to the owner, which hides an option with `hidden`. What
35
35
  * counts as a match differs between a select and a command palette.
36
36
  *
37
+ * An option with `href` is a link: choosing it follows the link and selects
38
+ * nothing, as an entry in a command palette that goes to a page does.
39
+ *
37
40
  * @since 0.1.0
38
41
  * @status stable
39
42
  * @category forms
@@ -44,7 +47,7 @@ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadle
44
47
  * @slot end - Content after the label, such as a shortcut or a count.
45
48
  * @slot start - Content before the label, such as an icon or an avatar.
46
49
  *
47
- * @event {HelixSelectEvent} helix-select - The option was chosen. Cancelable: canceling it leaves the selection as it was.
50
+ * @event {HelixSelectEvent} helix-select - The option was chosen. Cancelable: canceling it leaves the selection as it was, and keeps the browser from following an option's `href`.
48
51
  *
49
52
  * @csspart description - The description under the label, when there is one.
50
53
  * @csspart item - The row.
@@ -148,6 +151,28 @@ export declare class HelixHeadlessOption extends HelixElement {
148
151
  * Prevents the option from being chosen.
149
152
  */
150
153
  disabled: boolean;
154
+ /**
155
+ * Whether following the link downloads what it names. Only meaningful
156
+ * beside an `href`.
157
+ */
158
+ download: boolean;
159
+ /**
160
+ * The URL the option links to. Setting it makes the option a link: choosing
161
+ * it, with a press, Enter or a click, follows the link and selects nothing,
162
+ * as React Aria's items with `href` do. Command, Control and Shift open it
163
+ * where they open a link, in a new tab or a new window.
164
+ *
165
+ * `helix-select` is said first with the `href` in it, and canceling it keeps
166
+ * the browser from following, so a client-side router follows it instead.
167
+ *
168
+ * **The option stays an option**, and an anchor is never drawn inside it:
169
+ * `role="option"` sits on the host, where a listbox's
170
+ * `aria-activedescendant` can name it, and an `<a>` inside would be exposed
171
+ * as a link within the option and take its name (Menu Item's `href` records
172
+ * the measurement). The link is followed through an anchor made for the
173
+ * moment, which `followLink` describes.
174
+ */
175
+ href: string;
151
176
  /**
152
177
  * A shorter name for this choice, for where the row does not fit.
153
178
  *
@@ -161,12 +186,30 @@ export declare class HelixHeadlessOption extends HelixElement {
161
186
  * nothing for every choice.
162
187
  */
163
188
  label: string;
189
+ /**
190
+ * The link's `rel`. Set it whenever you set `target="_blank"`.
191
+ */
192
+ rel: string;
164
193
  /**
165
194
  * Whether this choice is currently chosen.
166
195
  *
167
196
  * Written by the listbox.
168
197
  */
169
198
  selected: boolean;
199
+ /**
200
+ * Where the link opens, such as `_blank`, for an option with an `href`.
201
+ */
202
+ target: string;
203
+ /**
204
+ * The words a reader types to reach this choice, where the row's own words
205
+ * are not them: typeahead, a filter and a combobox match against it, and a
206
+ * select's trigger shows it.
207
+ *
208
+ * For a row whose content is a picture, an avatar beside a name and a role,
209
+ * or a name written in another script, as React Aria's `textValue`. Empty
210
+ * reads the row's words, then `label`.
211
+ */
212
+ textValue: string;
170
213
  /**
171
214
  * The value that identifies the option, and that a form submits.
172
215
  */
@@ -210,11 +253,17 @@ export declare class HelixHeadlessOption extends HelixElement {
210
253
  * it is in every comparison too.
211
254
  *
212
255
  * **The `label` where the slot holds nothing**, which is what the row shows
213
- * then, so the six read the words a reader sees.
256
+ * then, so the six read the words a reader sees. `text-value` wins over both,
257
+ * being the consumer saying outright what the words are.
214
258
  */
215
259
  get text(): string;
216
- /** Sends `helix-select` for this option. Does nothing when it is disabled. */
217
- select(): void;
260
+ /**
261
+ * Sends `helix-select` for this option. Does nothing when it is disabled.
262
+ *
263
+ * An option with an `href` then follows its link, unless the event was
264
+ * canceled, with the keys `modifiers` names held.
265
+ */
266
+ select(modifiers?: LinkModifiers): void;
218
267
  /**
219
268
  * Mirrors the chosen state where a stylesheet can select on it.
220
269
  *