@fusion.dev/helix-headless 0.1.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
@@ -10,259 +10,31 @@ import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
- /* Side-effect import, because this renders a listbox in its own shadow root and
14
- a custom element registers on import. The path is relative, so this registers
15
- the base listbox and the styled file's identical import registers the styled
16
- one; `tier` picks which name the template asks for. */
17
13
  import '../../listbox/index.js';
18
- /* And the tags, for the same reason: `multiple` renders one per chosen option
19
- and a custom element registers on import. */
20
14
  import '../../tag/index.js';
21
- /**
22
- * Whether an edit added characters rather than taking them away.
23
- *
24
- * `beforeinput` and `input` both carry `inputType`, and it is the platform's
25
- * own answer to what the reader just did: `insertText` for typing,
26
- * `insertFromPaste` for a paste, and one of several `delete*` for the ways of
27
- * removing. Anything without one is treated as an insertion, because the only
28
- * events with no `inputType` here are the ones a script dispatches, and a test
29
- * or a consumer setting the value means it to behave like typing.
30
- */
31
15
  const isInsertion = (event) => !event.inputType?.startsWith('delete');
32
- /**
33
- * @summary A field that filters a list as it is typed into.
34
- *
35
- * @description
36
- * **The list is `helix-listbox`, and the filtering is its own.** It already
37
- * owns narrowing, the empty state, the highlight, `aria-activedescendant` and
38
- * scrolling an option into view. Rewriting them here would be a second copy to
39
- * keep in step; what is left for this element is the field, the panel and the
40
- * traffic between them.
41
- *
42
- * **The text and the value are two things.** What a reader types is a filter,
43
- * not an answer: it narrows the list and is thrown away. The value is whichever
44
- * option they then choose, and the field is rewritten to that option's words
45
- * once they have. A combobox that treated the typing as the value would submit
46
- * "jav" to a form, which is the mistake a plain input makes and the reason this
47
- * component exists.
48
- *
49
- * That is also why the field is emptied of its filter on close rather than left
50
- * holding it: a control reopened still narrowed by the last visit's search is a
51
- * control whose second use starts from the first one's leftovers. Select
52
- * records the same.
53
- *
54
- * **Focus stays in the field, which is the whole of what a combobox is.** The
55
- * arrows move the list without leaving the text, which is why the listbox has a
56
- * public `navigate`: the moving, the wrapping, the skipping of disabled options
57
- * and the seeding from the current value all stay in the one place that already
58
- * does them.
59
- *
60
- * `role="combobox"` on the input rather than on a wrapper, because the thing
61
- * the pattern describes is the text field, and `aria-controls` can name the
62
- * listbox because both are in this shadow root. The active option cannot be
63
- * named that way, being the consumer's light DOM, so it is linked by element
64
- * reference for the reason Listbox records at length.
65
- *
66
- * **With `multiple`, the chosen options are tags in the field.** A field that
67
- * says "Rust, Ruby, Go" is a sentence the reader has to parse to find out what
68
- * is in it, and offers nothing to press to take one out: Select does that today
69
- * and it is the weakest part of it. A tag apiece is one object per answer, each
70
- * with its own remove control, which is what the reader already believes they
71
- * are looking at.
72
- *
73
- * The panel stays open while more than one can be chosen, because a control
74
- * that shut after each pick is one the reader has to reopen to keep using. The
75
- * filter clears on each choice instead, so the next pick starts from the whole
76
- * list rather than from the last search.
77
- *
78
- * **`creatable` announces, and does not write.** The options are the consumer's
79
- * light DOM and nothing here puts an element into it: a component that grew the
80
- * consumer's markup would be a component their framework re-renders over. What
81
- * this does is offer a row saying what would be added, take the value when the
82
- * reader presses it, and say so with `helix-create`. A consumer who has to
83
- * validate first refuses the event and adds nothing.
84
- *
85
- * Which leaves a value naming no option, and that is what the words fall back
86
- * to: for anything chosen the field shows the option's text if there is one and
87
- * the value itself otherwise. No copy is stored, so a consumer who then adds a
88
- * real option with that value has its words take over on the next render.
89
- *
90
- * **`suggest` finishes the word, and only ever forwards.** The field completes
91
- * to the first option the typing begins, with the part the reader did not type
92
- * selected, so the next character replaces it and Delete removes it. Applied on
93
- * insertion alone: completing after a deletion would put back what the reader
94
- * just took out, and a field that cannot be shortened is a field that cannot be
95
- * corrected. Measured as the difference between `insertText` and the several
96
- * `delete*` input types, which is what the platform already says about which
97
- * happened.
98
- *
99
- * The filter stays what was typed rather than what was completed. Filtering on
100
- * the completion would narrow the list to the one option it came from, so a
101
- * reader typing "ru" would be shown "Rust" alone and never learn Ruby was
102
- * there.
103
- *
104
- * @since 0.1.0
105
- * @status experimental
106
- * @category forms
107
- * @aliases Autocomplete, Typeahead, Search select
108
- * @documentation https://helix.fusion.dev/components/combobox
109
- *
110
- * @slot - The options, which are `helix-option` elements.
111
- * @slot end - After it, for a unit or a clear button.
112
- * @slot indicator - Replaces the caret saying the field opens.
113
- * @slot start - Before the text, for an icon.
114
- *
115
- * @event {Event} change - Announced once the value has moved.
116
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the chosen option changes. Cancelable: refusing it takes the decision and nothing moves.
117
- * @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes. Cancelable: refusing it takes the decision and nothing moves.
118
- * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the panel opens. Cancelable: refusing it takes the decision and nothing moves.
119
- * @event {HelixCreateEvent} helix-create - Fired when the reader asks for something the list does not hold, and cancelable.
120
- * @event {Event} input - Announced once the value has moved.
121
- * @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
122
- *
123
- * @csspart control - The text field itself.
124
- * @csspart create - The row offering to add what was typed.
125
- * @csspart feedback - What is wrong with it.
126
- * @csspart field - The box around the text and its marks.
127
- * @csspart hint - Help under the field, while nothing is wrong.
128
- * @csspart indicator - The caret.
129
- * @csspart label - The question, over the field.
130
- * @csspart listbox - The list inside it.
131
- * @csspart listbox-empty - Shown when a filter has hidden everything.
132
- * @csspart listbox-feedback - What is wrong with it.
133
- * @csspart listbox-filter-icon - The mark beside that field.
134
- * @csspart listbox-filter-mark - The glyph beside the filter field, where none is slotted.
135
- * @csspart listbox-hint - Help under the list.
136
- * @csspart listbox-items - The list itself.
137
- * @csspart listbox-label - The question, over the list.
138
- * @csspart listbox-loading - Shown while the options are still on their way.
139
- * @csspart listbox-search - The region holding the field that narrows the list.
140
- * @csspart listbox-search-field - The field itself.
141
- * @csspart rest - The count of choices held beyond `max-tags`.
142
- * @csspart surface - The floating panel.
143
- * @csspart tag - One chosen option.
144
- * @csspart tag-dismiss - The remove button, when `dismissible`.
145
- * @csspart tag-dismiss-mark - The glyph on the dismiss control, where none is slotted.
146
- * @csspart tag-prefix - The category before the label.
147
- * @csspart tags - The row of chosen options, when `multiple` is set.
148
- */
149
16
  let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
150
17
  constructor() {
151
18
  super(...arguments);
152
- /* A consumer's `aria-describedby` names ids in their own tree and the control
153
- is in this shadow root, so the reference has to be carried down as an
154
- element rather than copied as an id. See `AriaForwardController`: measured
155
- in Chrome's own tree, the host held the description and the control the
156
- reader is actually on was exposed without it. */
157
19
  this.aria = new AriaForwardController(this, {
158
20
  target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
159
21
  });
160
- /**
161
- * What the browser may autofill in the text field, as a platform token.
162
- *
163
- * **Defaults to `off`, which is not the platform's default and is the
164
- * point.** The field filters a list rather than collecting a value, so a
165
- * browser offering to fill it with a saved address is competing for the same
166
- * keystrokes as the listbox. A consumer who wants autofill can still ask.
167
- */
168
22
  this.autocomplete = 'off';
169
- /**
170
- * Whether the reader may answer with something the list does not hold.
171
- *
172
- * The row appears while what is typed matches no option's words exactly, so
173
- * it is offered next to near misses too: a reader typing "Ruby on Rails" with
174
- * "Ruby" in the list wants the one they are typing, not the one they are
175
- * halfway through.
176
- */
177
23
  this.creatable = false;
178
- /**
179
- * What the create row says, before the words it would add.
180
- */
181
24
  this.createLabel = 'Add';
182
- /**
183
- * Words shown where the filter has hidden everything.
184
- */
185
25
  this.emptyText = 'No matches';
186
- /**
187
- * Accessible name for the list, which has no visible one of its own.
188
- */
189
26
  this.listLabel = 'Options';
190
- /**
191
- * Whether the options are still on their way.
192
- *
193
- * Passed straight to the list, which is where the rows are and so where the
194
- * message belongs.
195
- */
196
27
  this.loading = false;
197
- /**
198
- * What the list says while they are on their way.
199
- */
200
28
  this.loadingText = 'Loading';
201
- /**
202
- * How many tags to draw before the rest become a count. `multiple` only.
203
- *
204
- * `0` is all of them, and is the default: a cap that guesses is worse than no
205
- * cap, because the number that fits depends on the width of the field and the
206
- * length of what is in it, neither of which this element knows. What a cap is
207
- * genuinely for is a field in a row or a table cell, where a set of twelve
208
- * choices would push everything below it down a line at a time.
209
- *
210
- * The remainder is counted rather than hidden. `+3` is a fact a reader can act
211
- * on, where a row that silently stops is a field that appears to have lost
212
- * what was put in it. Date Input and Time Input carry the same property and
213
- * the same reasoning.
214
- */
215
29
  this.maxTags = 0;
216
- /**
217
- * Whether more than one option can be chosen.
218
- *
219
- * The list already does the work: `value` comes apart on commas, a press
220
- * toggles rather than replaces, and the arrows move without choosing. What
221
- * this adds is the tags, and keeping the panel open between picks.
222
- *
223
- * A comma is therefore not available inside an option's value, which is the
224
- * listbox's contract and not this element's to change.
225
- */
226
30
  this.multiple = false;
227
- /**
228
- * Text shown while nothing has been chosen.
229
- */
230
31
  this.placeholder = '';
231
- /**
232
- * What a tag's remove control is called, since the mark has no name.
233
- */
234
32
  this.removeLabel = 'Remove';
235
- /**
236
- * Whether the field finishes the word as it is typed.
237
- *
238
- * `suggest` rather than `autocomplete`, which is a real HTML attribute this
239
- * element already sets on the input it renders, and two things of one name
240
- * meaning different things on the same control is a question a consumer
241
- * should not have to ask.
242
- */
243
33
  this.suggest = false;
244
- /**
245
- * What the reader has typed, which narrows the list and is not the value.
246
- *
247
- * State rather than a property, because it is a keystroke rather than API: a
248
- * consumer sets `value` to say what is chosen, and nothing outside needs to
249
- * say what is half-typed.
250
- */
251
34
  this.typed = '';
252
- /**
253
- * The completed words, where the field is finishing one, and empty otherwise.
254
- *
255
- * Held apart from `typed` rather than replacing it, because they are needed
256
- * for different things at the same moment: this one is what the input shows,
257
- * `typed` is what the list is filtered by, and the selection between them is
258
- * what the reader is offered.
259
- */
260
35
  this.suggestion = '';
261
36
  this.panel = new PopoverController(this, {
262
37
  anchor: () => this.renderRoot?.querySelector('[part="field"]'),
263
- /* The surface *and* the field. Scoped to the surface alone, a press on the
264
- field counts as outside, so `pointerdown` closes the panel and the
265
- field's own handler reopens it on the `click` that follows. */
266
38
  boundary: () => [
267
39
  this.renderRoot?.querySelector('[part="surface"]'),
268
40
  this.renderRoot?.querySelector('[part="field"]'),
@@ -273,33 +45,16 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
273
45
  surface: () => this.renderRoot?.querySelector('[part="surface"]'),
274
46
  });
275
47
  this.handleInput = (event) => {
276
- // Stopped for the reason `Input.handleInput` gives: the inner control's own
277
- // `input` is composed and would reach a consumer beside this element's.
278
48
  event.stopPropagation();
279
49
  const typed = event.target.value;
280
50
  this.typed = typed;
281
- /* The list is narrowed before the completion is looked for, because the
282
- completion is the first thing still showing and that is not known until
283
- the filter has run. */
284
51
  this.listbox?.requestFilter(typed);
285
52
  this.suggestion = this.suggest && isInsertion(event) ? this.completionFor(typed) : '';
286
53
  if (!this.open) {
287
54
  this.requestOpen(true);
288
55
  }
289
56
  };
290
- /**
291
- * The keys the field answers, and the ones it hands on.
292
- *
293
- * Everything that moves within the list goes to the list, which is what
294
- * `navigate` is for. Escape and Tab are the field's, because they are about
295
- * the panel rather than about the options in it.
296
- */
297
57
  this.handleKeydown = (event) => {
298
- /* The input carries `disabled` too, so a reader never gets this far: the
299
- platform suppresses the event before any listener sees it. Said here
300
- anyway, because then the element's own state is what refuses rather than
301
- a detail of the node it happens to render, and a consumer dispatching a
302
- key at it gets the same answer a reader would. */
303
58
  if (this.isDisabled) {
304
59
  return;
305
60
  }
@@ -316,14 +71,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
316
71
  }
317
72
  return;
318
73
  }
319
- /* Backspace on an empty field takes the last tag off.
320
- *
321
- * Only when empty, so it never competes with deleting a character, and
322
- * only the last, because that is the one the caret is against. This is the
323
- * keyboard's version of pressing a tag's remove control, and without it a
324
- * reader who chose four things with the keyboard has to reach for a pointer
325
- * to undo one.
326
- */
327
74
  if (event.key === 'Backspace' && this.multiple && this.typed === '') {
328
75
  const last = this.values.at(-1);
329
76
  if (last !== undefined) {
@@ -332,45 +79,22 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
332
79
  }
333
80
  return;
334
81
  }
335
- /* Enter with nothing left in the list creates, where there is anything to
336
- create. Only then: with matches showing, Enter belongs to whichever one
337
- the arrows are on, and a reader pressing it over a highlighted "Rust"
338
- meant Rust. The row is still there to be pressed for the other case. */
339
82
  if (event.key === 'Enter' && this.creation !== '' && !this.showing) {
340
83
  event.preventDefault();
341
84
  this.create();
342
85
  return;
343
86
  }
344
- /* Opening on the first arrow, before the list is asked to move: a closed
345
- combobox pressed Down should show its options and land on the first,
346
- which is one gesture to the reader and two steps here. */
347
87
  if (!this.open && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
348
88
  this.requestOpen(true);
349
89
  }
350
90
  this.listbox?.navigate(event);
351
91
  };
352
- /**
353
- * Takes what the list announced and stops it there.
354
- *
355
- * The listbox says `helix-before-change` under its own name, and a consumer
356
- * listening on this element would otherwise hear two changes for one choice,
357
- * one of them naming a control they did not put on the page.
358
- */
359
92
  this.handleChange = (event) => {
360
93
  event.stopPropagation();
361
- /* From the event rather than from the listbox, which is one choice behind:
362
- it carries what is being *proposed*, dispatched before it is applied,
363
- which is what makes it refusable. The listbox never applies it anyway,
364
- since this element passes `.value` down and that is a consumer declaring
365
- ownership. */
366
94
  const { value } = event.detail;
367
- /* And nothing happens on a refusal. Refusing a change should leave the
368
- control where it was, not clear the filter and shut the panel as though
369
- it had been taken. */
370
95
  if (!this.requestValue(value)) {
371
96
  return;
372
97
  }
373
- // Either way the search is spent: it found what it was for.
374
98
  this.typed = '';
375
99
  this.suggestion = '';
376
100
  this.listbox?.requestFilter('');
@@ -378,24 +102,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
378
102
  this.close();
379
103
  }
380
104
  };
381
- /**
382
- * Anywhere in the field opens the list, and puts the caret where typing goes.
383
- *
384
- * On the field rather than on the `<input>`. The caret is a sibling of the
385
- * input and so is the tag strip, so a handler on the input alone leaves the
386
- * one part of the control that visibly says "this opens" doing nothing.
387
- * Anything drawn inside the field is the field.
388
- *
389
- * A tag is the exception, because a tag is its own control: its dismiss
390
- * removes a choice, and opening the list on the way past would leave the
391
- * reader looking at a panel they did not ask for. Read from the composed
392
- * path, since the press lands on a button inside the tag's shadow root and
393
- * the event has retargeted by the time it arrives here.
394
- *
395
- * It only ever opens. Closing is Escape, a choice, or leaving, which is what
396
- * the input already did and what a text field wants: a control that closed
397
- * when you clicked into the middle of your own typing would be unusable.
398
- */
399
105
  this.handleFieldClick = (event) => {
400
106
  if (this.isDisabled) {
401
107
  return;
@@ -410,21 +116,12 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
410
116
  this.requestOpen(true);
411
117
  }
412
118
  };
413
- /** One tag asking to be taken out of the set. */
414
119
  this.handleDismiss = (event) => {
415
120
  event.stopPropagation();
416
121
  const tag = event.currentTarget;
417
122
  this.drop(tag.dataset.value ?? '');
418
123
  };
419
124
  }
420
- /**
421
- * Which option is chosen, as an index, or `-1` where none is.
422
- *
423
- * Read off the listbox this renders, which owns the options and is where the
424
- * same two properties are implemented. Delegating rather than recomputing is
425
- * what keeps a composed control and the control inside it from answering
426
- * differently: they are one chooser to a consumer.
427
- */
428
125
  get selectedIndex() {
429
126
  return this.listbox?.selectedIndex ?? -1;
430
127
  }
@@ -434,63 +131,26 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
434
131
  listbox.selectedIndex = at;
435
132
  }
436
133
  }
437
- /** Which kind of chooser this is, in the platform's two words. */
438
134
  get type() {
439
135
  return this.multiple ? 'select-multiple' : 'select-one';
440
136
  }
441
137
  get listbox() {
442
138
  return this.renderRoot?.querySelector('[part="listbox"]') ?? null;
443
139
  }
444
- /** The values chosen, in the order they were chosen. */
445
140
  get values() {
446
141
  return this.value === '' ? [] : this.value.split(',');
447
142
  }
448
- /**
449
- * The chosen options themselves, for their words.
450
- *
451
- * Read off the options rather than stored, because the words are the
452
- * consumer's content and a copy of them goes stale the moment they change it.
453
- * Select records the same.
454
- *
455
- * In the value's own order, which is the order they were chosen. It has to
456
- * be: a created answer is not in the list at all, so there is no position in
457
- * the list to give it. Neither order ever reshuffles what is already there,
458
- * so a tag does not move out from under the pointer about to remove it.
459
- */
460
143
  get chosen() {
461
144
  const options = this.listbox?.options ?? [];
462
- /* Nothing at all is not the same as nothing matching.
463
- *
464
- * On the server there are no options to look in, because they are light DOM
465
- * children the server cannot see, and before the slot is assigned there are
466
- * none either. Falling back there would put the raw value on screen for one
467
- * paint and then swap it for the option's words, which reads as the field
468
- * correcting a mistake. With options present, a value none of them carries
469
- * is a created one, and showing it is the only way to show it at all. */
470
145
  const known = options.length > 0;
471
146
  const words = new Map(options.map((option) => [option.value, option.text]));
472
147
  return this.values
473
148
  .filter((value) => known || words.has(value))
474
149
  .map((value) => ({ text: words.get(value) ?? value, value }));
475
150
  }
476
- /**
477
- * Whether the filter has left anything to choose.
478
- *
479
- * Read off the options rather than asked of the listbox, which keeps its own
480
- * answer to this private. Both come from the same place: `hidden` is what the
481
- * filter writes onto an option, and it is public because hiding one by hand
482
- * is a thing a consumer may do.
483
- */
484
151
  get showing() {
485
152
  return (this.listbox?.options ?? []).some((option) => !option.hidden);
486
153
  }
487
- /**
488
- * What the create row would add, or nothing where there is nothing to add.
489
- *
490
- * Empty where the words already name an option exactly, case aside: offering
491
- * to add "Rust" under a list showing "Rust" is offering the reader a second
492
- * copy of what they are looking at.
493
- */
494
154
  get creation() {
495
155
  const typed = this.typed.trim();
496
156
  if (!this.creatable || typed === '') {
@@ -499,13 +159,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
499
159
  const exists = (this.listbox?.options ?? []).some((option) => option.text.trim().toLowerCase() === typed.toLowerCase());
500
160
  return exists ? '' : typed;
501
161
  }
502
- /**
503
- * Takes what the reader typed as an answer, once anyone listening allows it.
504
- *
505
- * Added to the set with `multiple` and replacing it without, which is the
506
- * same rule choosing an option follows: what differs about a created answer
507
- * is where it came from, not how many the field holds.
508
- */
509
162
  create() {
510
163
  const wanted = this.creation;
511
164
  if (wanted === '') {
@@ -525,26 +178,16 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
525
178
  this.close();
526
179
  }
527
180
  }
528
- /** The choices that get a tag, which is all of them until `maxTags` says not. */
529
181
  get shown() {
530
182
  const held = this.chosen;
531
183
  return this.maxTags > 0 ? held.slice(0, this.maxTags) : held;
532
184
  }
533
- /** How many are held beyond the ones shown. */
534
185
  get restCount() {
535
186
  return Math.max(0, this.chosen.length - this.shown.length);
536
187
  }
537
- /** What a single-choice field shows at rest, which the tags replace. */
538
188
  get word() {
539
189
  return this.chosen[0]?.text ?? '';
540
190
  }
541
- /**
542
- * Takes one value out of the set, which is what a tag's remove control does.
543
- *
544
- * `drop` rather than `remove`, which every element already has: `Element`
545
- * defines `remove()` and a method of that name here would be overriding it,
546
- * so `combobox.remove()` would stop taking the combobox out of the document.
547
- */
548
191
  drop(value) {
549
192
  this.requestValue(this.values.filter((one) => one !== value).join(','));
550
193
  }
@@ -555,14 +198,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
555
198
  this.requestOpen(false);
556
199
  this.renderRoot.querySelector('[part="control"]')?.focus();
557
200
  }
558
- /**
559
- * The words of the first option the typing begins, or nothing.
560
- *
561
- * Case-insensitively matched and returned in the option's own case, so a
562
- * reader typing "ru" is offered "Rust" rather than "rust": the completion is
563
- * a name from the list, and correcting the reader's capitals to match it is
564
- * the whole point of showing it.
565
- */
566
201
  completionFor(typed) {
567
202
  if (typed === '') {
568
203
  return '';
@@ -571,12 +206,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
571
206
  const match = (this.listbox?.options ?? []).find((option) => !option.hidden && !option.disabled && option.text.toLowerCase().startsWith(needle));
572
207
  return match && match.text.length > typed.length ? match.text : '';
573
208
  }
574
- /**
575
- * Reports the answer and the verdict on it.
576
- *
577
- * The value rather than the typed text, which is the whole point of the
578
- * component: a form that received "jav" got the filter, not an option.
579
- */
580
209
  sync() {
581
210
  this.internals.setFormValue(this.value === '' ? null : this.value);
582
211
  this.reportRequired(this.value === '', 'Choose one to continue.', 'control');
@@ -588,29 +217,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
588
217
  updated(changed) {
589
218
  super.updated?.(changed);
590
219
  this.sync();
591
- /* After the render, not during it: the input's value is a binding, so
592
- selecting a range of it before Lit has written it selects a range of the
593
- previous value.
594
-
595
- **And written here rather than left to the binding, because the binding
596
- stops writing.** Lit dirty-checks a property binding against the value it
597
- last wrote, not against the DOM, and typing changes the DOM underneath
598
- it. Type `r` into a list holding Ruby and the completion is `Ruby`, so
599
- the field is written `Ruby` with `uby` selected. Type `u` and the field
600
- is already `Ru` because the browser replaced the selection, and the
601
- completion is *still* `Ruby`: unchanged, so Lit writes nothing and
602
- `changed.has('suggestion')` is false, so nothing is selected either. The
603
- completion then appears on the first character and never again.
604
-
605
- Reconciled against what the input actually holds, which is the only thing
606
- that knows. */
607
220
  const control = this.renderRoot.querySelector('[part="control"]');
608
- /* The same expression the template binds, because this has to agree with it
609
- in both directions. Writing only when there is a completion puts the
610
- field right while one is showing and leaves it stale afterwards: this
611
- reconcile writes behind Lit's back, so Lit's own dirty-check then
612
- believes the DOM already holds whatever it last wrote and declines to
613
- correct it. */
614
221
  const wanted = this.multiple || this.open ? this.suggestion || this.typed : this.word;
615
222
  if (control && control.value !== wanted) {
616
223
  control.value = wanted;
@@ -623,28 +230,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
623
230
  }
624
231
  else {
625
232
  this.panel.hide();
626
- /* And the filter goes with it, for the reason the class note records: a
627
- control reopened still narrowed is one whose second use starts from the
628
- first one's leftovers.
629
-
630
- All three, not the listbox's copy alone. A surviving `typed` is
631
- invisible on close, because a closed single-choice field shows the
632
- chosen words instead, and comes back with the list behind it showing
633
- everything the moment it reopens. */
634
233
  this.typed = '';
635
234
  this.suggestion = '';
636
235
  this.listbox?.requestFilter('');
637
236
  }
638
237
  }
639
- /**
640
- * The list the field filters, behind a seam the styled tier can move.
641
- *
642
- * A method rather than a fragment inline, so a tier above can replace, wrap
643
- * or omit it without reimplementing the whole template. The tag is
644
- * interpolated rather than written, so `tier` resolves it: this renders the
645
- * headless element, and a styled subclass that sets `tier` renders the styled
646
- * one from the same template.
647
- */
648
238
  renderListbox() {
649
239
  return html `<${this.tag('listbox')}
650
240
  part="listbox"
@@ -671,15 +261,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
671
261
  <slot @slotchange=${() => this.requestUpdate()}></slot>
672
262
  </${this.tag('listbox')}>`;
673
263
  }
674
- /**
675
- * One chosen option, as a tag, behind a seam the styled tier can move.
676
- *
677
- * A method rather than a fragment inline, so a tier above can replace, wrap
678
- * or omit it without reimplementing the whole template. The tag is
679
- * interpolated rather than written, so `tier` resolves it: this renders the
680
- * headless element, and a styled subclass that sets `tier` renders the styled
681
- * one from the same template.
682
- */
683
264
  renderTag(option) {
684
265
  return html `<${this.tag('tag')}
685
266
  part="tag"
@@ -691,15 +272,6 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
691
272
  >${option.text}</${this.tag('tag')}
692
273
  >`;
693
274
  }
694
- /**
695
- * The caret saying the field opens, behind a seam.
696
- *
697
- * Slot fallback, so a consumer replaces it by slotting; and a method, so the
698
- * tier above replaces it by overriding. Those answer different people: the
699
- * slot is for the page and the method is for whoever is building the styled
700
- * layer, and a fallback with only the first is one a tier can restyle and
701
- * cannot change.
702
- */
703
275
  renderIndicator() {
704
276
  return html `<slot name="indicator">
705
277
  <svg part="indicator" fill="currentColor" viewBox=${caretDown.viewBox} aria-hidden="true">