@fusion.dev/helix-headless 0.2.0 → 0.4.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,116 +7,26 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, formatNumbers, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary Reads out which items of how many are on show.
12
- *
13
- * @description
14
- * Its own element rather than a part of `Pagination`, so a consumer decides
15
- * where it goes. Inside the run it could only ever be at one end of one row;
16
- * beside it, it goes above, below, in a table's caption, or on the other side of
17
- * the page, and the direction and order of the whole footer are the consumer's.
18
- *
19
- * **It repeats `page`, `page-size` and `total`, and the cost of that is skew
20
- * rather than verbosity.** Those three come from the consumer's own query, so
21
- * writing them on two elements is one variable used twice; what it risks is the
22
- * two disagreeing, and a count reading `76-100 of 248` beside a run showing page
23
- * 7 is the same failure this element's own clamping exists to prevent, arriving
24
- * from the outside instead.
25
- *
26
- * **Nothing removes it at this level, and that was measured rather than
27
- * assumed.** A coordinating root can only reach children the consumer laid out
28
- * by writing attributes onto them, which a framework reports as a hydration
29
- * error, or through context, which is dead on a server: `@lit/context`
30
- * subscribes from `connectedCallback`, and Lit SSR does not call it, so every
31
- * value is `undefined` in exactly the render this element exists to be part of.
32
- * Confirmed against Lit SSR directly. The fork is real: children in the light
33
- * DOM mean the consumer owns the layout and no root can feed them; children in a
34
- * shadow root mean a root feeds them by ordinary binding and owns the layout.
35
- *
36
- * **It resolves one level up, at a composite, and the state is why.** `page`,
37
- * `page-size` and `total` are facts about a *result*, not about a pagination, so
38
- * the element that should hold them once is the one that holds the result: a
39
- * data table composing a pagination inside itself, rather than a pagination root
40
- * invented to hold three pagination elements together. That root would have been
41
- * plumbing; a table's paging state is the thing itself. And because a composite
42
- * renders its parts into its own shadow root, it lands on the side of the fork
43
- * where binding works and the server renders, at no cost to anyone.
44
- *
45
- * Renders nothing without a `total` and a `page-size`. A wrong count is worse
46
- * than none: it is the one number in a footer a reader cannot check against
47
- * anything else on screen.
48
- *
49
- * @since 0.1.0
50
- * @status experimental
51
- * @aliases Result count, Range
52
- * @documentation https://helix.fusion.dev/components/pagination
53
- */
54
10
  let HelixHeadlessPaginationSummary = class HelixHeadlessPaginationSummary extends HelixElement {
55
11
  constructor() {
56
12
  super(...arguments);
57
- /**
58
- * The reading, with `{from}`, `{to}` and `{total}` filled in.
59
- *
60
- * A template rather than a prefix and a suffix, because word order is not
61
- * universal: `format="{total} 件中 {from}-{to} 件"` is one attribute here and
62
- * three concatenations anywhere else.
63
- *
64
- * **`format`, not `label`, and the difference is worth the rename.** `label`
65
- * across this library means the accessible name of a thing that has one:
66
- * `Pagination.label` names the landmark and `Pagination.Size.label` is the
67
- * words beside the control. This is neither. It is the whole visible content
68
- * of the element, so calling it `label` put one word on three jobs inside one
69
- * family. The root's templates are `pageLabel`, `statusLabel` and `gapLabel`,
70
- * which each label something; this labels nothing, it *is* the thing.
71
- */
72
13
  this.format = '{from}-{to} of {total}';
73
- /**
74
- * Locale for the numbers.
75
- *
76
- * Absent uses the runtime's own.
77
- */
78
14
  this.locale = '';
79
- /**
80
- * The page on show, one-based.
81
- */
82
15
  this.page = 1;
83
- /**
84
- * How many items are on a page.
85
- */
86
16
  this.pageSize = 0;
87
- /**
88
- * How many items there are across every page.
89
- */
90
17
  this.total = 0;
91
18
  }
92
- /** Held across every component asking for the same locale. */
93
19
  get numbers() {
94
20
  return formatNumbers(this.locale);
95
21
  }
96
22
  render() {
97
- /* An empty template rather than `nothing`, so the element generates a box
98
- with no text in it: a footer laid out as a grid keeps its column, and a
99
- count that appears when the total loads does not move the controls beside
100
- it. */
101
23
  if (this.total <= 0 || this.pageSize <= 0) {
102
24
  return html ``;
103
25
  }
104
26
  const size = Math.max(1, Math.floor(this.pageSize));
105
- /* **The page is clamped to the range, not just the window inside it.**
106
- Only the end was, so a `page` past the last one produced a start past the
107
- total and an end pinned to it: at page 99 of a 10-page result this read
108
- "2,451-248 of 248", a range running backwards. That is the worst kind of
109
- wrong here, because a count is the one number in a footer a reader cannot
110
- check against anything else on screen.
111
-
112
- A page past the end is not far-fetched either: it is a bookmarked query
113
- after the result shrank, and the run beside this clamps it silently, so
114
- the two would disagree with only this one visibly broken. */
115
27
  const pages = Math.max(1, Math.ceil(this.total / size));
116
28
  const page = Math.min(pages, Math.max(1, Math.floor(this.page) || 1));
117
29
  const from = (page - 1) * size + 1;
118
- /* And the end is clamped to the total, because the last page is rarely
119
- full: 248 items at 25 a page end at 248, not at 250. */
120
30
  const to = Math.min(this.total, from + size - 1);
121
31
  return html `${this.format
122
32
  .replaceAll('{from}', this.numbers(from))
@@ -7,141 +7,27 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { AriaForwardController, customElement, HelixCompleteEvent, HelixFormElement, MixinControllerValue, MixinField, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A short code, one character to a box.
12
- *
13
- * @description
14
- * **One `<input>` per cell, rather than one input drawn as cells.** A single
15
- * field autofills perfectly and needs a drawn caret nobody can style well;
16
- * separate fields give every box a real focus ring, a real caret and a real tab
17
- * stop for free. Autofill's gap is closed below rather than lived with.
18
- *
19
- * **Anything longer than one character is treated as a paste, wherever it
20
- * lands.** That covers the obvious case and two that are not: a browser
21
- * autofilling an SMS code drops the whole thing into whichever box is focused,
22
- * and a password manager does the same. So a six-character arrival in cell three
23
- * fills from cell three rather than being truncated to its first character,
24
- * which is what a per-cell `maxlength` would have done silently.
25
- *
26
- * **The code is one value to a form.** The cells are inside a shadow root and
27
- * submit nothing on their own; the host is form-associated and reports the
28
- * joined string, which is also what carries validity and answers a reset.
29
- *
30
- * `autocomplete="one-time-code"` is on the first cell only. It is a hint about
31
- * the field as a whole, and repeating it on six boxes offers a browser six
32
- * places to put one code.
33
- *
34
- * @since 0.1.0
35
- * @status stable
36
- * @category forms
37
- * @aliases PinField, OTP, One-time code, Verification code, InputPin
38
- * @documentation https://helix.fusion.dev/components/pin-input
39
- *
40
- * @event {Event} change - Announced with a filled code, and when an edited one loses focus.
41
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired as the code changes, and cancelable.
42
- * @event {HelixCompleteEvent} helix-complete - Fired when the last box is filled.
43
- * @event {Event} input - Announced once the value has moved.
44
- *
45
- * @csspart cell - One box.
46
- * @csspart feedback - What is wrong with it.
47
- * @csspart field - The row of boxes.
48
- * @csspart hint - Help under the boxes, while nothing is wrong.
49
- * @csspart label - The question, over the boxes.
50
- */
51
10
  let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerValue(MixinField(HelixFormElement)) {
52
11
  constructor() {
53
12
  super(...arguments);
54
- /* A consumer's `aria-describedby` names ids in their own tree and the control
55
- is in this shadow root, so the reference has to be carried down as an
56
- element rather than copied as an id. See `AriaForwardController`: measured
57
- in Chrome's own tree, the host held the description and the control the
58
- reader is actually on was exposed without it. */
59
13
  this.aria = new AriaForwardController(this, {
60
14
  target: () => this.renderRoot?.querySelector('[part="cell"]') ?? null,
61
15
  });
62
- /** Typed or dragged, so every edit is announced. See `announcesInput`. */
63
16
  this.announcesInput = true;
64
- /**
65
- * What the browser may autofill, reaching the first cell only.
66
- *
67
- * `one-time-code` is why this defaults to anything at all: it is the token
68
- * that lets a phone offer the code out of a message. Later cells stay `off`
69
- * for the reason in the class comment above, that six cells each offering
70
- * the same code is one offer made six times.
71
- */
72
17
  this.autocomplete = 'one-time-code';
73
- /**
74
- * Accessible name for one box, with `{index}` and `{length}` filled in.
75
- */
76
18
  this.cellLabel = 'Character {index} of {length}';
77
- /**
78
- * How many characters the code has.
79
- */
80
19
  this.length = 6;
81
- /**
82
- * Whether the characters are hidden, the way a password is.
83
- */
84
20
  this.mask = false;
85
- /**
86
- * What an empty box shows, usually a single dot.
87
- *
88
- * The platform's own placeholder, on each cell, rather than a character
89
- * drawn behind the field: it clears the moment anything is typed, it is
90
- * already excluded from the accessible value, and it works the same under
91
- * `mask`, where the cell is a password input and a hand-drawn glyph would
92
- * have to know that.
93
- *
94
- * Empty by default, because a row of boxes already reads as a row of boxes.
95
- */
96
21
  this.placeholder = '';
97
- /**
98
- * Whether the boxes refuse input while still submitting their value.
99
- */
100
22
  this.readOnly = false;
101
- /**
102
- * What a box accepts, which decides the keyboard a phone offers.
103
- *
104
- * `numeric` and `alphanumeric` also refuse anything else on the way in,
105
- * because a code that is digits is digits: letting a letter into a box a
106
- * reader cannot submit is a mistake they have to find and undo themselves.
107
- * `text` refuses nothing and only trims, so it is the mode for a code with a
108
- * hyphen in it.
109
- *
110
- * **`alphanumeric` is ASCII letters and digits**, which is narrower than the
111
- * word is in general and is the right narrowness here. A code of this kind
112
- * comes off a keypad, an SMS or an authenticator, and none of them emit `é`;
113
- * a filter that accepted it would let through a character the service on the
114
- * other end is going to reject. A consumer whose codes really do carry
115
- * marked letters wants `text`, which judges nothing.
116
- *
117
- * **Not reflected**: nothing selects on it, so reflecting only writes
118
- * `type="numeric"` onto every `<helix-pin-input>` ever rendered, an attribute
119
- * no consumer's markup had. The area rules carry the sentence "reflect only
120
- * what CSS selects on". The real inputs take it as a binding, which is what
121
- * makes the keyboard follow.
122
- */
123
23
  this.type = 'numeric';
124
24
  this.handleInput = (event) => {
125
- // Stopped for the reason `Input.handleInput` gives: the inner control's own
126
- // `input` is composed and would reach a consumer beside this element's.
127
25
  event.stopPropagation();
128
26
  const cell = event.target;
129
27
  const at = this.cells.indexOf(cell);
130
28
  if (at === -1) {
131
29
  return;
132
30
  }
133
- /* **An emptied box is a deletion, and without this branch it is
134
- unreachable.** A Backspace in a *filled* box is the browser's own: the
135
- keydown handler leaves it alone, the character goes, and `input` arrives
136
- carrying "". That falls through to `write`, which returns early on empty,
137
- and then `paint` puts the character straight back. Only real keystrokes
138
- reproduce it: dispatched events do not, which is why the specs pass over
139
- it.
140
-
141
- The rest collapse left, which is what the model already requires and what
142
- a single text field does: "123456" with the 2 deleted is "13456", not a
143
- hole. Focus stays where the reader was, so the box under the caret now
144
- holds what followed. */
145
31
  if (cell.value === '') {
146
32
  const characters = this.characters;
147
33
  characters[at] = '';
@@ -152,11 +38,7 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
152
38
  });
153
39
  return;
154
40
  }
155
- /* Longer than one character is a paste or an autofill, whichever box it
156
- landed in. A per-cell `maxlength` would have silently truncated it to the
157
- first character and lost the rest. */
158
41
  this.write(at, cell.value);
159
- // And put the box back, in case what arrived was refused or filtered.
160
42
  this.updateComplete.then(() => this.paint());
161
43
  };
162
44
  this.handlePaste = (event) => {
@@ -165,18 +47,9 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
165
47
  if (at === -1) {
166
48
  return;
167
49
  }
168
- // Taken here rather than left to `input`, so the browser does not first put
169
- // the whole string into one box and fire a change we then have to undo.
170
50
  event.preventDefault();
171
51
  this.write(at, event.clipboardData?.getData('text') ?? '');
172
52
  };
173
- /**
174
- * The keys that move between boxes, and the one that clears backwards.
175
- *
176
- * Backspace in an empty box goes back and clears the one it lands in, which
177
- * is what makes a code correctable without the reader hunting for the box
178
- * they meant. In a full box it does what it always does and is left alone.
179
- */
180
53
  this.handleKeydown = (event) => {
181
54
  const cell = event.target;
182
55
  const at = this.cells.indexOf(cell);
@@ -205,63 +78,12 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
205
78
  }
206
79
  event.preventDefault();
207
80
  };
208
- /**
209
- * The code as it stood when focus arrived, so a visit that changed nothing
210
- * says nothing.
211
- *
212
- * **Baselined on arrival, not at the last commit**, which is the platform's
213
- * reading. What the two do to a real browser, measured in Chromium against a
214
- * native `<input>` with real keystrokes:
215
- *
216
- * | | native | last commit | on arrival |
217
- * |---|---|---|---|
218
- * | types, leaves | `change` | `change` | `change` |
219
- * | script assigns unfocused, visit with no edit | silent | **`change`** | silent |
220
- * | script assigns unfocused, then edits | `change` | `change` | `change` |
221
- * | types a character and deletes it | silent | silent | silent |
222
- *
223
- * One row differed and it is the one that matters: a consumer who assigns
224
- * `value` and then has a reader tab through the row was told the reader had
225
- * entered a code. `change` on a field means "the reader had a go at this",
226
- * `MixinField` treats it as exactly that, and announcing a value the consumer
227
- * set themselves is a false positive rather than the safe half of a trade.
228
- *
229
- * **The narrower divergence that is left** is a script assigning while the row
230
- * is focused, where the platform stays silent because no user edit happened
231
- * and this commits on the way out. Closing it needs a dirty flag set from real
232
- * cell input rather than a baseline, which is more machinery than the case has
233
- * earned: a consumer writing to a field the reader is inside is already in a
234
- * fight with them.
235
- *
236
- * Seeded in `firstUpdated`, a code in the markup being committed by whoever
237
- * wrote it, and re-taken on every arrival from outside the row.
238
- */
239
81
  this.committed = '';
240
- /** Selecting on focus, so typing replaces rather than appending. */
241
82
  this.handleFocus = (event) => {
242
83
  ;
243
84
  event.target.select();
244
85
  };
245
- /**
246
- * Whether focus is currently somewhere inside the row.
247
- *
248
- * **The arrival is a transition, not a `relatedTarget` reading**, which is
249
- * what the exit still does. `relatedTarget` does not work on the way in for
250
- * two reasons and the second decides it. Every character moves focus to the
251
- * next box, so an unguarded `focusin` re-baselines on each keystroke and the
252
- * row can never report anything: measured, the three commit specs went silent
253
- * at once. And `focusin` carries `relatedTarget` in a browser and not in
254
- * happy-dom, so a guard written on it holds in production and not in the
255
- * specs, which is the worse half. This flag needs neither: entering is
256
- * `focusin` while it is `false`, and nothing else is.
257
- */
258
86
  this.within = false;
259
- /**
260
- * Takes the baseline when focus arrives from outside the row.
261
- *
262
- * The value at arrival is what the platform compares against, and the table
263
- * on `committed` is why.
264
- */
265
87
  this.handleFocusIn = () => {
266
88
  if (this.within) {
267
89
  return;
@@ -269,21 +91,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
269
91
  this.within = true;
270
92
  this.committed = this.value;
271
93
  };
272
- /**
273
- * Commits when focus leaves the row.
274
- *
275
- * `change` is one of the two events `MixinField` reads as "the reader has had
276
- * a go at this", so without this a code that was never completed is never
277
- * `touched` and its own `tooShort` message cannot show: two boxes of four
278
- * filled and tabbed away from is answered with nothing at all. `Input` gets
279
- * this for free by relaying its control's own `change`; here the cells each
280
- * fire one, none of them means the code is entered, and neither does any
281
- * single one of them mean the reader has left.
282
- *
283
- * **Focus leaving the row, and not a cell.** Every character moves focus to
284
- * the next box, so a per-cell blur would commit on every keystroke, which is
285
- * the fault this exists to fix pointed the other way.
286
- */
287
94
  this.handleFocusOut = (event) => {
288
95
  if (this.cells.includes(event.relatedTarget)) {
289
96
  return;
@@ -292,23 +99,19 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
292
99
  this.commit();
293
100
  };
294
101
  }
295
- /** Focus is delegated, so the host is never the focus target. */
296
102
  static { this.shadowRootOptions = {
297
103
  ...HelixFormElement.shadowRootOptions,
298
104
  delegatesFocus: true,
299
105
  }; }
300
- /** How many boxes there are, clamped once so every reader agrees. */
301
106
  get count() {
302
107
  return Math.max(1, Math.floor(this.length) || 1);
303
108
  }
304
109
  get cells() {
305
110
  return [...this.renderRoot.querySelectorAll('[part="cell"]')];
306
111
  }
307
- /** The code as characters, padded to the row so every box has one to show. */
308
112
  get characters() {
309
113
  return Array.from({ length: this.count }, (_, at) => this.value[at] ?? '');
310
114
  }
311
- /** Everything this box would accept, with the rest dropped. */
312
115
  clean(text) {
313
116
  const trimmed = text.trim();
314
117
  if (this.type === 'numeric') {
@@ -319,29 +122,11 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
319
122
  }
320
123
  return trimmed;
321
124
  }
322
- /**
323
- * Writes a run of characters from one box onward.
324
- *
325
- * The single path for typing, pasting and autofill alike, because they are
326
- * the same act at different lengths and splitting them is how the three
327
- * disagree about trimming, about the cursor, and about what happens at the
328
- * end.
329
- */
330
125
  write(from, text) {
331
126
  const cleaned = this.clean(text);
332
127
  if (cleaned === '') {
333
128
  return;
334
129
  }
335
- /* **A code has no holes in it**, so a character cannot land past the end of
336
- what is already there. Clamped rather than allowed, because the value is
337
- one compact string and a gap in the middle of it does not exist: unclamped,
338
- the boxes and the value disagree about where the reader typed.
339
-
340
- It reads correctly in both directions. Pasting a whole code while the
341
- caret happens to sit in box three fills from the start, which is what a
342
- reader pasting a code means. Pasting two characters into box three of a
343
- field with two already in it fills from box three, because that is where
344
- the end was. */
345
130
  const start = Math.min(from, this.value.length);
346
131
  const characters = this.characters;
347
132
  for (let at = 0; at < cleaned.length && start + at < this.count; at += 1) {
@@ -356,21 +141,10 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
356
141
  this.cells[filled]?.select();
357
142
  if (next.length === this.count) {
358
143
  this.dispatchEvent(new HelixCompleteEvent({ value: next }));
359
- /* And a native `change`, because a filled code is this control's
360
- commit. There is no inner control to relay one from: six boxes each
361
- fire their own, and none of them means the code is entered. */
362
144
  this.commit();
363
145
  }
364
146
  });
365
147
  }
366
- /**
367
- * Puts the value back into the boxes.
368
- *
369
- * The cells are uncontrolled between renders, because Lit will not rewrite an
370
- * input's `.value` when the bound string has not changed, and a refused
371
- * keystroke leaves the box holding a character the element rejected. This is
372
- * the same put-back `Input` does with `restoreValue`, once per box.
373
- */
374
148
  paint() {
375
149
  const characters = this.characters;
376
150
  for (const [at, cell] of this.cells.entries()) {
@@ -380,21 +154,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
380
154
  }
381
155
  }
382
156
  }
383
- /**
384
- * Announces the code as entered, from whichever moment reached it first.
385
- *
386
- * **One writer, because the fact has two moments and only one meaning.** A
387
- * filled code and a row being left are both "the reader is done with this".
388
- * Dispatching from either site directly is how a moment gets added that fires
389
- * without baselining.
390
- *
391
- * Silent when nothing moved since the baseline, which is what makes it safe
392
- * to call from both: completing a code that is already the committed one, or
393
- * leaving a row nobody edited, says nothing.
394
- *
395
- * `composed: false`, like the native event this stands in for. It is
396
- * dispatched on the host, so a consumer hears it.
397
- */
398
157
  commit() {
399
158
  if (this.value === this.committed) {
400
159
  return;
@@ -402,32 +161,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
402
161
  this.committed = this.value;
403
162
  this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
404
163
  }
405
- /**
406
- * Reports the code and a verdict on it.
407
- *
408
- * **A half-entered code is invalid whether or not it is required, and that is
409
- * the platform's answer rather than ours.** `required` guards `valueMissing`
410
- * and nothing guards `tooShort`, so an optional code is refused for being half
411
- * filled. Measured in Chromium against a native `<input minlength="4">` with
412
- * real keystrokes, which is the feature this is the same shape as:
413
- *
414
- * | | optional | required |
415
- * |---|---|---|
416
- * | typed `12` | `tooShort`, invalid | `tooShort`, invalid |
417
- * | left empty | valid | `valueMissing` |
418
- * | typed `1234` | valid | valid |
419
- *
420
- * So `required` decides whether *nothing* is acceptable, and the length rule
421
- * applies to anything that is not nothing. That is the right split for a code
422
- * as well: two of four characters is not an optional code omitted, it is a
423
- * code got wrong, and submitting it would send a fragment. The gate on the
424
- * message is `touched` rather than `required`, which is what stops a reader
425
- * being told off before they have had a go.
426
- *
427
- * `execCommand('insertText')` is not enough to measure this: `tooShort`
428
- * applies only to a value the user edited, and the first run of the probe
429
- * reported every case valid because nothing had set that flag.
430
- */
431
164
  sync() {
432
165
  this.internals.setFormValue(this.value);
433
166
  const first = this.cells[0];
@@ -443,16 +176,13 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
443
176
  else {
444
177
  this.internals.setValidity({}, '', first);
445
178
  }
446
- // Straight after, because validity moved without any property moving.
447
179
  this.refreshFeedback();
448
180
  }
449
- /** Puts the caret in the first empty box, which is where a reader resumes. */
450
181
  focus(options) {
451
182
  const cells = this.cells;
452
183
  const at = Math.min(this.value.length, cells.length - 1);
453
184
  cells[at]?.focus(options);
454
185
  }
455
- /** Empties every box and returns to the first. */
456
186
  clear() {
457
187
  this.requestValue('');
458
188
  this.updateComplete.then(() => {
@@ -471,12 +201,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
471
201
  }
472
202
  render() {
473
203
  const characters = this.characters;
474
- /* `renderGroupLabel` and not `renderLabel`, which is what every other
475
- grouped control here uses. `renderLabel` emits `for="control"` and no
476
- element in this template carries that id, because a pin input is a row of
477
- cells and not one control: the visible label pointed at nothing, so
478
- pressing it focused nothing and the group had no accessible name at all.
479
- The set names itself through `hostAria` instead. */
480
204
  return html `${this.renderGroupLabel()}
481
205
  <div
482
206
  part="field"