@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.
- package/custom-elements.json +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- 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">
|