@fusion.dev/helix-headless 0.2.0 → 0.3.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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -7,127 +7,33 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, HelixReadEvent, hostAria, MixinCollection, slotFilled, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A feed a reader comes back to, made of notifications they may not have seen.
12
- *
13
- * @description
14
- * **Slotted children rather than a data property.** A notification is a small
15
- * piece of somebody's own markup, with an avatar and a link and a button in it,
16
- * and there are tens of them rather than thousands, so writing them out lets an
17
- * item hold anything.
18
- *
19
- * **This does not virtualize, and it cannot.** Windowing needs the list to own
20
- * the data, and here the consumer owns it: N notifications is N elements by
21
- * construction. A feed that really does hold thousands wants a data table with a
22
- * custom cell, and this component will not grow into one.
23
- *
24
- * **Read state is reported, never invented.** Marking read is usually a round
25
- * trip: this element asks its items and each item asks the consumer, so a
26
- * refused `helix-read` leaves everything alone and a consumer who owns `read`
27
- * sees nothing change until they say so. `unread` is derived from the items and
28
- * written to an attribute, so a stylesheet and a badge can both read it without
29
- * either of them counting.
30
- *
31
- * @since 0.1.0
32
- * @status experimental
33
- * @category feedback
34
- * @aliases Notification feed, Inbox, Activity
35
- * @documentation https://helix.fusion.dev/components/notifications
36
- *
37
- * @slot - The notifications, as `helix-notification` children.
38
- * @slot footer - Below the list, for a link to everything.
39
- * @slot header - Above the list, for a title and the mark-all control.
40
- *
41
- * @event {HelixReadEvent} helix-read - Fired by an item, and by this element when everything is marked read. Cancelable.
42
- *
43
- * @csspart empty - Shown when there is nothing in the feed.
44
- * @csspart footer - The region below the list.
45
- * @csspart header - The region above the list.
46
- * @csspart list - The list itself.
47
- * @csspart loading - Shown while the feed is still on its way.
48
- * @csspart mark-all - The control that marks everything read.
49
- */
50
10
  let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinCollection(HelixElement) {
51
11
  constructor() {
52
12
  super(...arguments);
53
- /**
54
- * Accessible name for the feed.
55
- */
56
13
  this.label = '';
57
- /**
58
- * Whether to draw a control that marks everything read.
59
- */
60
14
  this.markAll = false;
61
- /**
62
- * Label for that control.
63
- */
64
15
  this.markAllLabel = 'Mark all as read';
65
- /** Items arrive from the light DOM, which changes no property here. */
66
16
  this.handleSlotChange = () => {
67
17
  this.assign();
68
18
  this.requestUpdate();
69
19
  };
70
- /**
71
- * An item's own read event, heard on the way past, so the count follows.
72
- *
73
- * `unread` is derived from the items and nothing about an item changing tells
74
- * Lit to look again. Not stopped: the event is the consumer's to hear, and
75
- * this element is a bystander that happens to need to redraw.
76
- */
77
20
  this.handleRead = () => {
78
- /* After the item has applied it, not before: read synchronously the count
79
- would be the one from before the change and the badge would trail by one
80
- every time. */
81
21
  queueMicrotask(() => this.requestUpdate());
82
22
  };
83
23
  this.handleMarkAll = () => {
84
24
  this.markEveryoneRead();
85
25
  };
86
26
  }
87
- /**
88
- * Nothing is assigned by name, so the items land in a list and the rest does not.
89
- *
90
- * `role="listitem"` under anything but a `list` is a list item belonging to
91
- * nothing, and the items are ordinary children a consumer writes with no
92
- * `slot`. Manual assignment is the only way to wrap them without editing the
93
- * consumer's markup, which is a change a React consumer's hydration reports
94
- * and does not patch up.
95
- */
96
27
  static { this.shadowRootOptions = {
97
28
  ...HelixElement.shadowRootOptions,
98
29
  slotAssignment: 'manual',
99
30
  }; }
100
- /**
101
- * How many items are unread, counted from the items themselves.
102
- *
103
- * Read-only because it is derived: a count the consumer could set is a second
104
- * source of truth for something the items already know.
105
- *
106
- * **The attribute is written in `updated`, never reflected.**
107
- * `@property({ reflect: true })` on a getter with no setter reflects once: Lit
108
- * reflects what is in `changedProperties`, and nothing puts a derived getter
109
- * there after the first update. Measured with two items, one marked read: the
110
- * property answered 1 and the attribute still said 2, so a badge reading the
111
- * attribute showed a count for an item the reader had just opened.
112
- */
113
31
  get unread() {
114
32
  return this.items.filter((item) => !item.read).length;
115
33
  }
116
- /** The notifications declared as children, in the order they are written. */
117
34
  get items() {
118
- /* `?? []` is the server. Lit's element shim carries attributes and not a
119
- tree, so `children` is undefined there, and this getter is reached from
120
- `unread` during the first update. */
121
35
  return [...(this.children ?? [])].filter((child) => 'isNotification' in child && child.isNotification === true);
122
36
  }
123
- /**
124
- * Marks every unread item read, and reports how many actually moved.
125
- *
126
- * **Through each item's own request rather than by writing its property**,
127
- * which is what makes one refusal refuse one item: a consumer who owns some
128
- * items and not others gets the right answer for each, and a server that
129
- * rejects one does not silently mark the rest.
130
- */
131
37
  markEveryoneRead() {
132
38
  if (!this.dispatchEvent(new HelixReadEvent({ read: true }))) {
133
39
  return 0;
@@ -166,56 +72,13 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
166
72
  super.willUpdate(changed);
167
73
  hostAria(this.internals, { label: this.label || 'Notifications', role: 'region' });
168
74
  }
169
- /**
170
- * Says whether anything in the feed has media, so every item can hold the
171
- * column open whether or not it has one of its own.
172
- *
173
- * **A fact no item can answer for itself**, which is why it is here. An item
174
- * knows about its own media slot and nothing about its neighbors, so a feed
175
- * where some rows have an avatar and some do not draws two different left
176
- * edges for its text: measured on a four item feed, the rows without media
177
- * started their text 32 pixels to the left of the rows with it. The styled
178
- * tier turns this into a width; this tier ships no stylesheet and is
179
- * unaffected.
180
- *
181
- * Read off the light DOM rather than off the items, so it does not depend on
182
- * which children have upgraded by the time this runs.
183
- */
184
75
  markMedia() {
185
76
  this.toggleState('media', this.items.some((item) => item.querySelector(':scope > [slot="media"]') !== null));
186
77
  }
187
- /**
188
- * Says on each region whether anything is in it.
189
- *
190
- * **A stylesheet cannot ask this, and the obvious selector fails twice over.**
191
- * `[part="header"]:not(:has(> slot:has(*)))`: `:has()` may not be nested
192
- * inside `:has()`, so the selector fails to parse and the whole rule is
193
- * discarded; and a slot's assigned nodes are not its DOM children, so
194
- * `slot:has(*)` reports whether the slot has *fallback* rather than whether a
195
- * consumer filled it, and would have been false either way. Both verified in a
196
- * browser: `insertRule` throws a `SyntaxError` on the first, and an assigned
197
- * element still leaves `slot.matches(':has(*)')` false.
198
- *
199
- * `slotFilled` is what the rest of the library already asks with. It is said
200
- * on the region inside this shadow root rather than on the host, because an
201
- * attribute written onto the host is a change to markup the consumer rendered,
202
- * which React reports on hydration and does not patch up.
203
- */
204
78
  markEmpty() {
205
- /* The header is not empty while this element is drawing its own control into
206
- it. */
207
79
  this.markRegion('header', ['header'], '[part="mark-all"]');
208
80
  this.markRegion('footer', ['footer']);
209
81
  }
210
- /**
211
- * One region, hidden when its slot holds nothing and nothing of ours fills it.
212
- *
213
- * `hidden` beside the `data-empty` mirror: `data-empty` is the hook the styled
214
- * sheet reads, and this tier ships no sheet, so an empty region left in the box
215
- * tree would be a gap a headless consumer cannot close without knowing to look
216
- * for it. The styled rule stays too, because that sheet sets `display` on these
217
- * parts and therefore outranks the UA's `[hidden]` one.
218
- */
219
82
  markRegion(part, slots, occupied) {
220
83
  const region = this.renderRoot?.querySelector(`[part="${part}"]`);
221
84
  const empty = !slots.some((name) => slotFilled(this.renderRoot?.querySelector(`slot[name="${name}"]`))) && !(occupied && this.renderRoot?.querySelector(occupied));
@@ -224,20 +87,9 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
224
87
  }
225
88
  updated(changed) {
226
89
  super.updated(changed);
227
- /* Before `markEmpty`, which reads what is assigned: on a feed with no items
228
- nothing else ever schedules a second update, so a header asked about
229
- before its slot was filled stays hidden for the life of the element. The
230
- empty feed is the one state where the header is the only thing on screen. */
231
90
  this.assign();
232
91
  this.markEmpty();
233
92
  this.markMedia();
234
- /* Written after the render rather than reflected during it, so it follows
235
- every change rather than only the first.
236
-
237
- **Zero is the absence of the attribute, not `unread="0"`.** Writing it at
238
- rest puts an attribute on the host that a consumer's server never sent,
239
- and `updated` is no later than hydration: it is the same microtask
240
- checkpoint `firstUpdated` runs in. Only a departure from resting writes. */
241
93
  const unread = this.unread > 0 ? String(this.unread) : null;
242
94
  if (this.getAttribute('unread') !== unread) {
243
95
  if (unread === null) {
@@ -9,154 +9,24 @@ import { minus, plus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
- /**
13
- * How far a pointer travels for one step of the value, in pixels.
14
- *
15
- * Four, measured against the alternatives rather than picked: at one the value
16
- * runs away from the pointer and a field with `step="1"` crosses a hundred in
17
- * the width of a word, and at ten a reader has to cross the viewport to reach
18
- * a useful number. Four is roughly a character's width at the sizes this is set
19
- * in, so a step reads as "about one glyph of travel".
20
- */
21
12
  const SCRUB_PITCH = 4;
22
- /**
23
- * And the same step over four times the travel, which is what fine means here.
24
- *
25
- * Fine lengthens the distance rather than shrinking the increment. A drag that
26
- * produced values between the steps would be the one input in the library that
27
- * can reach a number its own arrow keys cannot, and `step` would stop being the
28
- * grid the control is on.
29
- */
30
13
  const SCRUB_FINE = 16;
31
- /**
32
- * @summary A number a reader types or steps to.
33
- *
34
- * @description
35
- * **A family rather than `Input`'s `type="number"`, and rather than a part of
36
- * it.** `Input` already takes that type and will keep it: a bare numeric field
37
- * in a form is a text field with a different keyboard, and that is all it needs
38
- * to be. This is a different control. It renders two buttons, binds four keys
39
- * that `Input` does not, and owns clamping and stepping, so it is an alternative
40
- * and not a variant. Namespacing it under `Input` would say it is contained by
41
- * one, which is what `IconButton` is top-level for.
42
- *
43
- * **The field is `type="text"` with `inputmode="decimal"`, and that is a
44
- * measurement rather than a preference.** Asked of a real browser, `type=number`
45
- * fails three ways this control cannot live with:
46
- *
47
- * ```
48
- * typed "12e" into type=number → .value === ""
49
- * focused, wheeled over it → 5 became 4
50
- * selectionStart on type=number → null
51
- * same keystrokes into type=text → .value === "12e"
52
- * ```
53
- *
54
- * The first is fatal on its own: the element cannot see what the reader typed,
55
- * so a controlled value is destroyed by a keystroke and there is nothing left to
56
- * correct or to report. The second changes a value because a page scrolled past.
57
- * The third means the caret cannot be managed at all, which any correcting or
58
- * formatting field needs. `inputmode` is what keeps the phone keypad, which is
59
- * the one thing `type=number` was giving.
60
- *
61
- * Validity is still the platform's, through `min`, `max` and `step` mirrored
62
- * onto the host's own `setValidity`. What is not mirrored is the field's
63
- * `type`, because a text field has no opinion about numbers.
64
- *
65
- * **Stepping is separate from typing.** A reader mid-way through "1" of "12"
66
- * has not asked for anything to be clamped, so the value is only pulled into
67
- * range when they step, blur, or the form asks. Clamping on every keystroke is
68
- * what makes a field that cannot be typed backwards into.
69
- *
70
- * @since 0.1.0
71
- * @status experimental
72
- * @category forms
73
- * @aliases NumberField, Stepper, InputNumber
74
- * @documentation https://helix.fusion.dev/components/number-input
75
- *
76
- * @slot end - After it.
77
- * @slot start - Before the number, for a unit or an icon.
78
- *
79
- * @event {Event} change - Relayed from the field once the reader has finished.
80
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired as the value changes, and cancelable.
81
- * @event {Event} input - Announced once the value has moved.
82
- *
83
- * @csspart control - The field itself.
84
- * @csspart decrement - The control that steps down.
85
- * @csspart decrement-mark - The glyph on the step down control.
86
- * @csspart feedback - What is wrong with it.
87
- * @csspart field - The box holding everything.
88
- * @csspart hint - Help under the field, while nothing is wrong.
89
- * @csspart increment - The control that steps up.
90
- * @csspart increment-mark - The glyph on the step up control.
91
- * @csspart label - The name over the field, which is also the scrub grip.
92
- */
93
14
  let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinControllerValue(MixinField(HelixFormElement)) {
94
15
  constructor() {
95
16
  super(...arguments);
96
- /* A consumer's `aria-describedby` names ids in their own tree and the control
97
- is in this shadow root, so the reference has to be carried down as an
98
- element rather than copied as an id. See `AriaForwardController`: measured
99
- in Chrome's own tree, the host held the description and the control the
100
- reader is actually on was exposed without it. */
101
17
  this.aria = new AriaForwardController(this, {
102
18
  target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
103
19
  });
104
- /** Typed or dragged, so every edit is announced. See `announcesInput`. */
105
20
  this.announcesInput = true;
106
- /**
107
- * What the browser may autofill here, as one of the platform's own tokens.
108
- *
109
- * Empty leaves the attribute off, which is the browser's default and is what
110
- * lets autofill work at all. Hardcoding `off` here is what stops a reader
111
- * having a birthday or a postal code filled in for them, with no way for a
112
- * consumer to ask for it.
113
- */
114
21
  this.autocomplete = '';
115
- /**
116
- * Accessible name for the control that steps down.
117
- */
118
22
  this.decrementLabel = 'Decrease';
119
- /**
120
- * Accessible name for the control that steps up.
121
- */
122
23
  this.incrementLabel = 'Increase';
123
- /**
124
- * Shown while the field is empty.
125
- *
126
- * Not a label: it goes when typing starts.
127
- */
128
24
  this.placeholder = '';
129
- /**
130
- * Whether the field refuses input while still submitting its value.
131
- */
132
25
  this.readOnly = false;
133
- /**
134
- * Whether a grip is shown that changes the value when it is dragged.
135
- *
136
- * The label is what is dragged, so this adds no chrome at all. Still opt-in,
137
- * because it changes what a press on the label does and a reader who drags
138
- * across a word expecting to select it should get that everywhere it has not
139
- * been asked for. Where it earns its place is a value a reader tunes rather
140
- * than states: a duration, a weight, a count they are looking for rather than
141
- * one they already know.
142
- */
143
26
  this.scrub = false;
144
- /**
145
- * How far one step moves.
146
- */
147
27
  this.step = 1;
148
- /** Where the pointer went down, and what the value was there. */
149
28
  this.scrubFrom = 0;
150
29
  this.scrubValue = 0;
151
- /**
152
- * Press, and the value follows the pointer.
153
- *
154
- * Captured, so a drag that leaves the grip keeps going: a reader tuning a
155
- * number moves further than a 20 pixel handle and the gesture ending because
156
- * they overshot it would be the control refusing the very thing it offered.
157
- * `preventDefault` because a press inside a field otherwise starts a text
158
- * selection, which drags a blue wash across the label at the same time.
159
- */
160
30
  this.handleScrubDown = (event) => {
161
31
  if (this.isDisabled || this.readOnly) {
162
32
  return;
@@ -167,15 +37,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
167
37
  this.scrubValue = this.numeric ?? this.min ?? 0;
168
38
  event.preventDefault();
169
39
  };
170
- /**
171
- * Read from where the press began, never from the last frame.
172
- *
173
- * Accumulating a delta per move drifts: every rounding to a step throws away
174
- * the remainder, and a slow drag across a hundred events throws it away a
175
- * hundred times, so the value and the pointer disagree by more the longer
176
- * anyone holds. From the origin the arithmetic is the same on every frame and
177
- * the value can only be where the pointer says it is.
178
- */
179
40
  this.handleScrubMove = (event) => {
180
41
  const grip = event.currentTarget;
181
42
  if (!grip.hasPointerCapture(event.pointerId)) {
@@ -186,10 +47,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
186
47
  const pitch = event.altKey ? SCRUB_FINE : SCRUB_PITCH;
187
48
  const steps = Math.round((event.clientX - this.scrubFrom) / pitch);
188
49
  const next = String(this.clamp(this.scrubValue + steps * unit));
189
- /* Compared before it is sent. A drag produces a move event per frame and
190
- most of them land on the step the value is already on, and announcing
191
- those would be `helix-input` firing sixty times a second saying nothing
192
- changed. */
193
50
  if (next === this.value) {
194
51
  return;
195
52
  }
@@ -199,43 +56,18 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
199
56
  restoreValue(control, this.value);
200
57
  }
201
58
  };
202
- /** Let go, and the capture goes with it. */
203
59
  this.handleScrubEnd = (event) => {
204
60
  const grip = event.currentTarget;
205
61
  if (grip.hasPointerCapture(event.pointerId)) {
206
62
  grip.releasePointerCapture(event.pointerId);
207
63
  }
208
64
  };
209
- /**
210
- * Every keystroke, cancelable, and put back where it is refused.
211
- *
212
- * Not clamped here. A reader typing "12" is at "1" for one keystroke, and a
213
- * field with `min="10"` that corrected them at that moment could never be
214
- * typed into at all. Range is settled on blur, on a step, and at submit.
215
- */
216
65
  this.handleInput = (event) => {
217
- // Stopped for the reason `Input.handleInput` gives: the inner control's own
218
- // `input` is composed and would reach a consumer beside this element's.
219
66
  event.stopPropagation();
220
67
  const control = event.target;
221
68
  this.requestValue(control.value);
222
69
  restoreValue(control, this.value);
223
70
  };
224
- /**
225
- * Leaving the field is where a half-typed number becomes a number.
226
- *
227
- * Blank is left blank, because an empty optional field is a legitimate answer
228
- * and filling it with `min` would put a number in a form the reader never
229
- * gave.
230
- *
231
- * **And text that is not a number is left alone.** Clearing it means typing
232
- * `2.7e` and tabbing away empties the field: the reader looks back and their
233
- * entry is gone with nothing saying why, and the likeliest cause is a fat
234
- * finger on a number they had almost finished. Nothing is lost by keeping it,
235
- * because `sync` has already marked it `badInput`, so the field shows as wrong
236
- * and the form refuses to submit. Destroying the evidence of a mistake is not
237
- * the same as preventing one.
238
- */
239
71
  this.handleBlur = () => {
240
72
  const control = this.control;
241
73
  const parsed = this.numeric;
@@ -245,18 +77,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
245
77
  this.requestValue(String(this.clamp(parsed)));
246
78
  restoreValue(control, this.value);
247
79
  };
248
- /** Relayed, because `change` does not cross a shadow boundary on its own. */
249
80
  this.handleChange = (event) => {
250
81
  event.stopPropagation();
251
82
  this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
252
83
  };
253
- /**
254
- * The four keys a spinner answers, which `Input` does not bind.
255
- *
256
- * `Home` and `End` only where there is a bound to go to: without one they are
257
- * the text field's own keys and moving the caret to the start of the number is
258
- * what a reader pressing them meant.
259
- */
260
84
  this.handleKeydown = (event) => {
261
85
  if (event.key === 'ArrowUp') {
262
86
  this.stepUp();
@@ -269,9 +93,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
269
93
  }
270
94
  else if (event.key === 'PageDown') {
271
95
  this.stepDown(true);
272
- /* Guarded the way `nudge` is. A read-only field still receives keydown,
273
- so an unguarded Home on one with a `min` silently rewrites the value
274
- that the arrows beside it correctly refuse to touch. */
275
96
  }
276
97
  else if (event.key === 'Home' &&
277
98
  this.min !== undefined &&
@@ -288,11 +109,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
288
109
  else {
289
110
  return;
290
111
  }
291
- // Or the arrows scroll the page and Home jumps to the top of it.
292
112
  event.preventDefault();
293
113
  };
294
114
  }
295
- /** Focus is delegated to the field, so the host is never the focus target. */
296
115
  static { this.shadowRootOptions = {
297
116
  ...HelixFormElement.shadowRootOptions,
298
117
  delegatesFocus: true,
@@ -300,7 +119,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
300
119
  get control() {
301
120
  return this.renderRoot.querySelector('[part="control"]');
302
121
  }
303
- /** The value as a number, or `null` where the field holds nothing usable. */
304
122
  get numeric() {
305
123
  if (this.value.trim() === '') {
306
124
  return null;
@@ -308,33 +126,15 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
308
126
  const parsed = Number(this.value);
309
127
  return Number.isFinite(parsed) ? parsed : null;
310
128
  }
311
- /**
312
- * A value pulled into range and onto the step grid.
313
- *
314
- * The grid is anchored at `min` where there is one, so `min="1" step="2"`
315
- * gives 1, 3, 5 rather than 0, 2, 4. Rounded to the step's own precision,
316
- * because stepping 0.1 in binary floating point otherwise produces
317
- * `0.30000000000000004` and puts it in the field.
318
- */
319
129
  clamp(value) {
320
130
  const step = Math.abs(this.step) || 1;
321
131
  const anchor = this.min ?? 0;
322
132
  const stepped = anchor + Math.round((value - anchor) / step) * step;
323
133
  const floored = this.min === undefined ? stepped : Math.max(this.min, stepped);
324
134
  const capped = this.max === undefined ? floored : Math.min(this.max, floored);
325
- /* Decimal places from the step and the anchor together: a step of 0.5 from
326
- an anchor of 0.25 lands on 0.75, which needs two rather than the one the
327
- step alone would give. */
328
135
  const places = Math.max(decimals(step), decimals(anchor));
329
136
  return Number(capped.toFixed(places));
330
137
  }
331
- /**
332
- * Moves by whole steps, from where the reader is.
333
- *
334
- * From `min` when the field is empty, so the first press of an arrow lands
335
- * somewhere meaningful rather than on zero: an empty field with `min="1"`
336
- * stepping to 0 would be out of range before the reader had typed anything.
337
- */
338
138
  nudge(steps) {
339
139
  if (this.isDisabled || this.readOnly) {
340
140
  return;
@@ -348,37 +148,12 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
348
148
  restoreValue(control, this.value);
349
149
  }
350
150
  }
351
- /** One step up, or `PageUp`'s larger one. */
352
151
  stepUp(large = false) {
353
152
  this.nudge(large ? (this.largeStep ?? 10) : 1);
354
153
  }
355
- /**
356
- * Steps the value down, by `largeStep` when `large` is set.
357
- *
358
- * The same path a press of the down arrow takes, so a consumer driving this
359
- * from their own control gets the clamping and the announcement rather than
360
- * having to repeat both.
361
- */
362
154
  stepDown(large = false) {
363
155
  this.nudge(large ? -(this.largeStep ?? 10) : -1);
364
156
  }
365
- /**
366
- * The label is the surface, which is where the convention already is.
367
- *
368
- * Not a grip of its own at the leading edge, which is the obvious answer and
369
- * is a second control in a field that already has three. Every tool that does
370
- * this well drags the *name* of the value: the label is present on every
371
- * field, costs no chrome, and a reader who wants to know what they are
372
- * dragging is already looking at it.
373
- *
374
- * Overridden rather than handled on the host, because the handlers have to
375
- * sit on the element that takes the capture. The markup is the mixin's, said
376
- * again here, which is the duplication #76 exists to remove.
377
- *
378
- * A label with nothing in it is still nothing. `scrub` on a field whose label
379
- * is empty leaves no surface, which is honest: there is nowhere to put the
380
- * gesture and inventing somewhere is how the grip happened.
381
- */
382
157
  renderLabel() {
383
158
  if (!this.scrub) {
384
159
  return super.renderLabel();
@@ -398,15 +173,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
398
173
  ><slot name="label">${this.label || hidden}</slot></label
399
174
  >`;
400
175
  }
401
- /**
402
- * Reports the value and a verdict on it.
403
- *
404
- * Computed here rather than copied off the field, which is the one place this
405
- * differs from `Input`: the field is text, so the browser has no opinion about
406
- * whether it is a number in range. The flags are the platform's own names, so
407
- * a consumer reading `validity.rangeOverflow` finds what they expect and the
408
- * message is what a native spinner would have said.
409
- */
410
176
  sync() {
411
177
  const control = this.control;
412
178
  this.internals.setFormValue(this.value);
@@ -429,23 +195,16 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
429
195
  else {
430
196
  this.internals.setValidity({}, '', control);
431
197
  }
432
- // Straight after, because validity moved without any property moving.
433
198
  this.refreshFeedback();
434
199
  }
435
- /** Puts the caret in the field, the way a label pointing at one would. */
436
200
  focus(options) {
437
201
  this.control?.focus(options);
438
202
  }
439
- /** Selects the number, so typing replaces it. */
440
203
  select() {
441
204
  this.control?.select();
442
205
  }
443
206
  willUpdate(changed) {
444
207
  super.willUpdate(changed);
445
- /* Never before the first update, which is the one the server does: there is
446
- no render root to query there, and `querySelector` is not merely empty on
447
- Lit's element shim, it is undefined. There is also no caret to keep before
448
- anything has rendered. */
449
208
  if (!this.hasUpdated) {
450
209
  return;
451
210
  }
@@ -479,13 +238,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
479
238
  </svg>
480
239
  </button>
481
240
  <slot name="start"></slot>
482
- ${
483
- /* `role="spinbutton"` with the range on it, which is what tells a
484
- screen reader this is a number in a range rather than a line of
485
- text. The three `aria-value*` attributes are what the native
486
- `type=number` would have carried, and they have to be written here
487
- because the field is text. */
488
- ''}
241
+ ${''}
489
242
  <input
490
243
  part="control"
491
244
  id="control"
@@ -524,13 +277,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
524
277
  </div>
525
278
  ${this.renderHint()}${this.renderFeedback()}`;
526
279
  }
527
- /**
528
- * The value as a number, or `NaN` while the control is empty.
529
- *
530
- * `NaN` and not zero, which is the trap the platform already avoids: `Number('')`
531
- * is zero, so an empty control would report a number it does not hold and a
532
- * form would submit it.
533
- */
534
280
  get valueAsNumber() {
535
281
  return asNumber(this.value);
536
282
  }
@@ -572,14 +318,6 @@ HelixHeadlessNumberInput = __decorate([
572
318
  customElement('helix-headless-number-input')
573
319
  ], HelixHeadlessNumberInput);
574
320
  export { HelixHeadlessNumberInput };
575
- /**
576
- * How many decimal places a number is written with.
577
- *
578
- * Read off the string rather than computed, because the question is about how
579
- * the consumer wrote the step and not about what the binary can represent.
580
- * `1e-7` is handled separately: its string form has no dot at all, so counting
581
- * after one would say zero and round every result to a whole number.
582
- */
583
321
  const decimals = (value) => {
584
322
  const text = String(value);
585
323
  const exponent = /e-(\d+)$/.exec(text);
@@ -7,35 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A named run of options inside a listbox.
12
- *
13
- * @description
14
- * A heading over a set, and nothing else: it holds no selection and no value,
15
- * because grouping is presentation over a list whose owner is the listbox. Its
16
- * options are still the listbox's options, which is what keeps the keyboard
17
- * moving through a grouped list exactly as it moves through a flat one.
18
- *
19
- * `role="group"` named by the heading, so a screen reader announces which run
20
- * an option is in without the heading becoming something to land on.
21
- *
22
- * @since 0.1.0
23
- * @status experimental
24
- * @aliases OptGroup, Section
25
- * @documentation https://helix.fusion.dev/components/option-group
26
- *
27
- * @slot - The options in this run.
28
- * @slot label - Richer than a string.
29
- *
30
- * @csspart items - The run itself.
31
- * @csspart label - The heading.
32
- */
33
10
  let HelixHeadlessOptionGroup = class HelixHeadlessOptionGroup extends HelixElement {
34
11
  constructor() {
35
12
  super(...arguments);
36
- /**
37
- * The heading over the run.
38
- */
39
13
  this.label = '';
40
14
  }
41
15
  willUpdate(changed) {