@fusion.dev/helix-headless 0.14.0 → 0.15.1

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.
@@ -4,12 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, customElement, drawIcon, HelixFormElement, holdCaret, MixinControllerValue, MixinField, MixinTokens, restoreValue, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, drawIcon, HelixFormElement, holdCaret, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, PopoverController, restoreValue, } from '@fusion.dev/helix-core';
8
8
  import { x } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  const asksToGo = (node) => typeof node?.dismiss === 'function';
12
- let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinTokens(MixinControllerValue(MixinField(HelixFormElement))) {
12
+ const isOption = (node) => 'active' in node && 'value' in node;
13
+ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
13
14
  constructor() {
14
15
  super(...arguments);
15
16
  this.aria = new AriaForwardController(this, {
@@ -20,11 +21,27 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
20
21
  this.clearLabel = 'Clear';
21
22
  this.placeholder = '';
22
23
  this.over = 0;
24
+ this.activeAt = -1;
25
+ this.suggesting = false;
26
+ this.dismissed = false;
27
+ this.panel = new PopoverController(this, {
28
+ anchor: () => this.renderRoot?.querySelector('[part="field"]'),
29
+ boundary: () => [
30
+ this.renderRoot?.querySelector('[part="surface"]'),
31
+ this.renderRoot?.querySelector('[part="field"]'),
32
+ ],
33
+ offset: () => 4,
34
+ onDismiss: () => this.requestOpen(false),
35
+ placement: 'bottom-start',
36
+ surface: () => this.renderRoot?.querySelector('[part="surface"]'),
37
+ });
23
38
  this.handleInput = (event) => {
24
39
  event.stopPropagation();
25
40
  const control = event.target;
26
41
  this.requestValue(control.value);
27
42
  restoreValue(control, this.value);
43
+ this.dismissed = false;
44
+ this.settleOpen();
28
45
  };
29
46
  this.handleChange = (event) => {
30
47
  event.stopPropagation();
@@ -39,7 +56,68 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
39
56
  }
40
57
  };
41
58
  this.handleKeydown = (event) => {
42
- this.tokenKeydown(event, this.value);
59
+ if (this.tokenKeydown(event, this.value)) {
60
+ return;
61
+ }
62
+ if (!this.suggesting) {
63
+ return;
64
+ }
65
+ switch (event.key) {
66
+ case 'ArrowDown':
67
+ event.preventDefault();
68
+ this.move(1);
69
+ return;
70
+ case 'ArrowUp':
71
+ event.preventDefault();
72
+ this.move(-1);
73
+ return;
74
+ case 'Home':
75
+ if (this.open) {
76
+ event.preventDefault();
77
+ this.activeAt = 0;
78
+ }
79
+ return;
80
+ case 'End':
81
+ if (this.open) {
82
+ event.preventDefault();
83
+ this.activeAt = this.suggestions.length - 1;
84
+ }
85
+ return;
86
+ case 'Escape':
87
+ if (this.open) {
88
+ event.preventDefault();
89
+ event.stopPropagation();
90
+ this.dismissed = true;
91
+ this.activeAt = -1;
92
+ this.requestOpen(false);
93
+ }
94
+ return;
95
+ case 'Enter': {
96
+ const active = this.suggestions[this.activeAt];
97
+ if (!this.open || !active) {
98
+ return;
99
+ }
100
+ event.preventDefault();
101
+ active.select();
102
+ return;
103
+ }
104
+ default:
105
+ }
106
+ };
107
+ this.handleSuggestions = () => {
108
+ this.refreshSuggesting();
109
+ this.settleOpen();
110
+ };
111
+ this.handleFocusIn = () => {
112
+ this.settleOpen();
113
+ };
114
+ this.handleFocusOut = (event) => {
115
+ const next = event.relatedTarget;
116
+ if (next instanceof Node && this.contains(next)) {
117
+ return;
118
+ }
119
+ this.activeAt = -1;
120
+ this.requestOpen(false);
43
121
  };
44
122
  this.handleTokens = () => {
45
123
  this.over = this.hideOverflow(this.tokens);
@@ -58,6 +136,56 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
58
136
  get showsClear() {
59
137
  return this.clearable && this.value !== '' && !this.isDisabled;
60
138
  }
139
+ get suggestions() {
140
+ const slot = this.renderRoot?.querySelector('slot[name="suggestions"]');
141
+ const found = [];
142
+ for (const node of slot?.assignedElements({ flatten: true }) ?? []) {
143
+ if (isOption(node)) {
144
+ found.push(node);
145
+ continue;
146
+ }
147
+ for (const child of node.children) {
148
+ if (isOption(child)) {
149
+ found.push(child);
150
+ }
151
+ }
152
+ }
153
+ return found.filter((option) => !option.disabled);
154
+ }
155
+ get selectionDirection() {
156
+ return this.control?.selectionDirection ?? 'none';
157
+ }
158
+ get selectionEnd() {
159
+ return this.control?.selectionEnd ?? 0;
160
+ }
161
+ get selectionStart() {
162
+ return this.control?.selectionStart ?? 0;
163
+ }
164
+ setSelectionRange(start, end, direction) {
165
+ this.control?.setSelectionRange(start, end, direction);
166
+ }
167
+ settleOpen() {
168
+ const focused = this.matches(':focus-within') || this.contains(document.activeElement);
169
+ this.requestOpen(this.suggesting && !this.dismissed && !this.isDisabled && focused);
170
+ }
171
+ move(by) {
172
+ const options = this.suggestions;
173
+ if (options.length === 0) {
174
+ return;
175
+ }
176
+ if (!this.open) {
177
+ this.dismissed = false;
178
+ this.settleOpen();
179
+ }
180
+ const from = this.activeAt;
181
+ this.activeAt =
182
+ from < 0 ? (by > 0 ? 0 : options.length - 1) : (from + by + options.length) % options.length;
183
+ }
184
+ refreshSuggesting() {
185
+ const options = this.suggestions;
186
+ this.suggesting = options.length > 0;
187
+ this.activeAt = this.activeAt < options.length ? this.activeAt : -1;
188
+ }
61
189
  removeLastToken() {
62
190
  const last = this.tokens?.assignedElements({ flatten: true }).at(-1);
63
191
  if (asksToGo(last)) {
@@ -74,23 +202,46 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
74
202
  this.releaseCaret = holdCaret(control);
75
203
  }
76
204
  this.handleTokens();
205
+ this.refreshSuggesting();
77
206
  }
78
207
  firstUpdated(changed) {
79
208
  super.firstUpdated(changed);
80
209
  this.handleTokens();
210
+ this.refreshSuggesting();
81
211
  }
82
212
  updated(changed) {
83
213
  super.updated(changed);
84
214
  this.releaseCaret?.();
85
215
  this.releaseCaret = undefined;
86
216
  this.internals.setFormValue(this.value);
217
+ this.markActive();
218
+ if (this.open) {
219
+ this.panel.show();
220
+ }
221
+ else {
222
+ this.panel.hide();
223
+ }
224
+ this.settleOpen();
225
+ }
226
+ markActive() {
227
+ const options = this.suggestions;
228
+ const active = this.open ? options[this.activeAt] : undefined;
229
+ for (const [at, option] of options.entries()) {
230
+ option.active = this.open && at === this.activeAt;
231
+ }
232
+ const control = this.control;
233
+ if (control && 'ariaActiveDescendantElement' in control) {
234
+ ;
235
+ control.ariaActiveDescendantElement = active ?? null;
236
+ }
237
+ active?.scrollIntoView({ block: 'nearest' });
87
238
  }
88
239
  focus(options) {
89
240
  this.control?.focus(options);
90
241
  }
91
242
  render() {
92
243
  return html `${this.renderLabel()}
93
- <div part="field">
244
+ <div part="field" @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
94
245
  <slot name="start"></slot>
95
246
  ${this.renderTokenRow(html `<slot name="tags" @slotchange=${this.handleTokens}></slot>`, this.over)}
96
247
  <input
@@ -101,6 +252,10 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
101
252
  name=${this.name || nothing}
102
253
  placeholder=${this.placeholder || nothing}
103
254
  aria-describedby=${this.describedBy ?? nothing}
255
+ role=${this.suggesting ? 'combobox' : nothing}
256
+ aria-autocomplete=${this.suggesting ? 'list' : nothing}
257
+ aria-controls=${this.suggesting ? 'suggestions' : nothing}
258
+ aria-expanded=${this.suggesting ? (this.open ? 'true' : 'false') : nothing}
104
259
  ?disabled=${this.isDisabled}
105
260
  ?required=${this.required}
106
261
  @input=${this.handleInput}
@@ -119,6 +274,14 @@ let HelixHeadlessFilterInput = class HelixHeadlessFilterInput extends MixinToken
119
274
  </button>`
120
275
  : nothing}
121
276
  </div>
277
+ <!-- **Always rendered, and hidden rather than absent**, for the reason the
278
+ token row is: a slot that is not in the tree assigns nothing, so a panel
279
+ built only once there were options could never learn that there are
280
+ any. A manual popover puts it in the top layer, so it is not clipped by
281
+ a scrolling table or an overflow on an ancestor. -->
282
+ <div part="surface" id="suggestions" role="listbox" popover="manual" ?hidden=${!this.open}>
283
+ <slot name="suggestions" @slotchange=${this.handleSuggestions}></slot>
284
+ </div>
122
285
  ${this.renderHint()}${this.renderFeedback()}`;
123
286
  }
124
287
  };
@@ -134,6 +297,12 @@ __decorate([
134
297
  __decorate([
135
298
  state()
136
299
  ], HelixHeadlessFilterInput.prototype, "over", void 0);
300
+ __decorate([
301
+ state()
302
+ ], HelixHeadlessFilterInput.prototype, "activeAt", void 0);
303
+ __decorate([
304
+ state()
305
+ ], HelixHeadlessFilterInput.prototype, "suggesting", void 0);
137
306
  HelixHeadlessFilterInput = __decorate([
138
307
  customElement('helix-headless-filter-input')
139
308
  ], HelixHeadlessFilterInput);
@@ -167,17 +167,25 @@ let HelixHeadlessTimeInput = class HelixHeadlessTimeInput extends MixinControlle
167
167
  if (!control) {
168
168
  return;
169
169
  }
170
- const value = this.value;
170
+ const held = this.held;
171
+ const floor = toMinutes(this.min) ?? 0;
172
+ const ceiling = toMinutes(this.max) ?? 1439;
171
173
  let message = '';
172
- if (value !== '' && !isTime(value)) {
173
- message = 'Enter a time as HH:mm.';
174
+ if (held.length > 0 && !held.every(isTime)) {
175
+ message =
176
+ this.mode === undefined
177
+ ? 'Enter a time as HH:mm.'
178
+ : 'Enter times as HH:mm, separated by commas.';
174
179
  }
175
- else if (value !== '' && toMinutes(value) < (toMinutes(this.min) ?? 0)) {
180
+ else if (held.some((one) => (toMinutes(one) ?? 0) < floor)) {
176
181
  message = `Choose a time at or after ${writeTime(this.min, this.locale)}.`;
177
182
  }
178
- else if (value !== '' && toMinutes(value) > (toMinutes(this.max) ?? 1439)) {
183
+ else if (held.some((one) => (toMinutes(one) ?? 0) > ceiling)) {
179
184
  message = `Choose a time at or before ${writeTime(this.max, this.locale)}.`;
180
185
  }
186
+ else if (this.mode === 'range' && held.length > 2) {
187
+ message = 'A range is two times.';
188
+ }
181
189
  if (message === '') {
182
190
  this.internals.setValidity(control.validity, control.validationMessage, control);
183
191
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-headless",
3
3
  "description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
4
- "version": "0.14.0",
4
+ "version": "0.15.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "bugs": "https://github.com/project-fusion/fusion/issues",
47
47
  "dependencies": {
48
- "@fusion.dev/helix-core": "0.14.0",
49
- "@fusion.dev/helix-icons": "0.14.0",
48
+ "@fusion.dev/helix-core": "0.15.1",
49
+ "@fusion.dev/helix-icons": "0.15.1",
50
50
  "uqr": "^0.1.3"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@custom-elements-manifest/analyzer": "^0.11.0",
54
- "@fusion.dev/helix-cem": "0.14.0",
55
- "@fusion.dev/helix-testing": "0.14.0",
54
+ "@fusion.dev/helix-cem": "0.15.1",
55
+ "@fusion.dev/helix-testing": "0.15.1",
56
56
  "@fusion.dev/scripts": "0.0.0-dev",
57
57
  "@happy-dom/global-registrator": "^20.0.0",
58
58
  "@lit-labs/ssr": "^3.3.0",
package/types/jsx.d.ts CHANGED
@@ -5800,6 +5800,12 @@ export type HelixHeadlessFilterInputElementEvent<E = Event> = TypedEvent<
5800
5800
  /** `helix-before-change` event type */
5801
5801
  export type HelixHeadlessFilterInputHelixBeforeChangeElementEvent =
5802
5802
  HelixHeadlessFilterInputElementEvent<HelixBeforeChangeEvent>;
5803
+ /** `helix-before-close` event type */
5804
+ export type HelixHeadlessFilterInputHelixBeforeCloseElementEvent =
5805
+ HelixHeadlessFilterInputElementEvent<HelixBeforeCloseEvent>;
5806
+ /** `helix-before-open` event type */
5807
+ export type HelixHeadlessFilterInputHelixBeforeOpenElementEvent =
5808
+ HelixHeadlessFilterInputElementEvent<HelixBeforeOpenEvent>;
5803
5809
 
5804
5810
  export type HelixHeadlessFilterInputProps = {
5805
5811
  /** Whether the field offers a button that empties it.
@@ -5817,6 +5823,23 @@ them. */
5817
5823
 
5818
5824
  Not a label: it goes when typing starts. */
5819
5825
  placeholder?: (HelixHeadlessFilterInput["placeholder"]) | undefined;
5826
+ /** Whether the element is open.
5827
+
5828
+ One property, not two, so `dialog.open` answers the question anyone
5829
+ asking it meant. Writing always applies; `<helix-dialog open>` is the
5830
+ same statement in markup.
5831
+
5832
+ **Not reflected, and that is required rather than tidy.** Reflection
5833
+ writes the resolved state back as an attribute, so an element that opened
5834
+ itself would put `open` on its own host: cloned, serialized for
5835
+ hydration, or diffed by a framework, it comes back holding whatever the
5836
+ last reader did rather than what the author wrote. `:state(open)` carries
5837
+ the resolved state for stylesheets, which is what the mirror is for. */
5838
+ open?: (HelixHeadlessFilterInput["open"]) | undefined;
5839
+ /** Initial open state for uncontrolled use, where nothing declared one. */
5840
+ "default-open"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
5841
+ /** Initial open state for uncontrolled use, where nothing declared one. */
5842
+ defaultOpen?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
5820
5843
  /** What the control says once everything is showing. */
5821
5844
  "collapse-label"?: (HelixHeadlessFilterInput["collapseLabel"]) | undefined;
5822
5845
  /** What the control says once everything is showing. */
@@ -5883,8 +5906,18 @@ Here rather than on each control. */
5883
5906
  "onhelix-before-change"?: ((
5884
5907
  e: HelixHeadlessFilterInputHelixBeforeChangeElementEvent,
5885
5908
  ) => void) | undefined;
5909
+ /** Fired before the suggestions go, and cancelable. */
5910
+ "onhelix-before-close"?: ((
5911
+ e: HelixHeadlessFilterInputHelixBeforeCloseElementEvent,
5912
+ ) => void) | undefined;
5913
+ /** Fired before the suggestions show, and cancelable. */
5914
+ "onhelix-before-open"?: ((
5915
+ e: HelixHeadlessFilterInputHelixBeforeOpenElementEvent,
5916
+ ) => void) | undefined;
5886
5917
  /** Announced once the text has moved. */
5887
5918
  oninput?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
5919
+ /** Says the panel moved, carrying `newState` and `oldState`. */
5920
+ ontoggle?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
5888
5921
  };
5889
5922
 
5890
5923
  export type HelixHeadlessFilterInputSolidJsProps = {
@@ -5903,6 +5936,23 @@ them. */
5903
5936
 
5904
5937
  Not a label: it goes when typing starts. */
5905
5938
  "prop:placeholder"?: (HelixHeadlessFilterInput["placeholder"]) | undefined;
5939
+ /** Whether the element is open.
5940
+
5941
+ One property, not two, so `dialog.open` answers the question anyone
5942
+ asking it meant. Writing always applies; `<helix-dialog open>` is the
5943
+ same statement in markup.
5944
+
5945
+ **Not reflected, and that is required rather than tidy.** Reflection
5946
+ writes the resolved state back as an attribute, so an element that opened
5947
+ itself would put `open` on its own host: cloned, serialized for
5948
+ hydration, or diffed by a framework, it comes back holding whatever the
5949
+ last reader did rather than what the author wrote. `:state(open)` carries
5950
+ the resolved state for stylesheets, which is what the mirror is for. */
5951
+ "prop:open"?: (HelixHeadlessFilterInput["open"]) | undefined;
5952
+ /** Initial open state for uncontrolled use, where nothing declared one. */
5953
+ "bool:default-open"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
5954
+ /** Initial open state for uncontrolled use, where nothing declared one. */
5955
+ "prop:defaultOpen"?: (HelixHeadlessFilterInput["defaultOpen"]) | undefined;
5906
5956
  /** What the control says once everything is showing. */
5907
5957
  "attr:collapse-label"?: (HelixHeadlessFilterInput["collapseLabel"]) | undefined;
5908
5958
  /** What the control says once everything is showing. */
@@ -5968,8 +6018,18 @@ Here rather than on each control. */
5968
6018
  "on:helix-before-change"?: ((
5969
6019
  e: HelixHeadlessFilterInputHelixBeforeChangeElementEvent,
5970
6020
  ) => void) | undefined;
6021
+ /** Fired before the suggestions go, and cancelable. */
6022
+ "on:helix-before-close"?: ((
6023
+ e: HelixHeadlessFilterInputHelixBeforeCloseElementEvent,
6024
+ ) => void) | undefined;
6025
+ /** Fired before the suggestions show, and cancelable. */
6026
+ "on:helix-before-open"?: ((
6027
+ e: HelixHeadlessFilterInputHelixBeforeOpenElementEvent,
6028
+ ) => void) | undefined;
5971
6029
  /** Announced once the text has moved. */
5972
6030
  "on:input"?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
6031
+ /** Says the panel moved, carrying `newState` and `oldState`. */
6032
+ "on:toggle"?: ((e: HelixHeadlessFilterInputElementEvent) => void) | undefined;
5973
6033
 
5974
6034
  /** Set the innerHTML of the element */
5975
6035
  innerHTML?: (string) | undefined;
@@ -20567,6 +20627,20 @@ export type CustomElements = {
20567
20627
  * `helix-dismiss` its own remove control fires, so a consumer who handled the
20568
20628
  * pointer has already handled the keyboard.
20569
20629
  *
20630
+ * **It can suggest what to type next, and it never learns the grammar.** Slot
20631
+ * `helix-option` elements into `suggestions` and the field shows them, moves
20632
+ * through them with the arrows, and lets Enter press one; the option fires its
20633
+ * own `helix-select` and the consumer decides what choosing meant. Deciding
20634
+ * *what* to suggest is theirs too: keys before a colon and values after it are
20635
+ * the same panel with different contents, which is all a two-level filter is,
20636
+ * and the parse that tells them apart belongs where the domain does.
20637
+ *
20638
+ * **The panel opens when there is something in it and closes when there is
20639
+ * not**, rather than on a choice. A reader who picks a key wants its values
20640
+ * next, and closing on the choice would shut the panel between two renders. Use
20641
+ * `selectionStart` and `setSelectionRange` to write a completion back and put
20642
+ * the caret after it, or the next parse reads the wrong term.
20643
+ *
20570
20644
  * ## Attributes & Properties
20571
20645
  *
20572
20646
  * Component attributes and properties that can be applied to the element or by using JavaScript.
@@ -20581,6 +20655,19 @@ export type CustomElements = {
20581
20655
  * - `placeholder`: Shown while the field is empty.
20582
20656
  *
20583
20657
  * Not a label: it goes when typing starts.
20658
+ * - `open`: Whether the element is open.
20659
+ *
20660
+ * One property, not two, so `dialog.open` answers the question anyone
20661
+ * asking it meant. Writing always applies; `<helix-dialog open>` is the
20662
+ * same statement in markup.
20663
+ *
20664
+ * **Not reflected, and that is required rather than tidy.** Reflection
20665
+ * writes the resolved state back as an attribute, so an element that opened
20666
+ * itself would put `open` on its own host: cloned, serialized for
20667
+ * hydration, or diffed by a framework, it comes back holding whatever the
20668
+ * last reader did rather than what the author wrote. `:state(open)` carries
20669
+ * the resolved state for stylesheets, which is what the mirror is for.
20670
+ * - `default-open`/`defaultOpen`: Initial open state for uncontrolled use, where nothing declared one.
20584
20671
  * - `collapse-label`/`collapseLabel`: What the control says once everything is showing.
20585
20672
  * - `expandable`: Whether the count is a control that shows the rest.
20586
20673
  * - `expand-label`/`expandLabel`: What the collapsed count is called, since `+3` names nothing.
@@ -20620,6 +20707,20 @@ export type CustomElements = {
20620
20707
  * imperatively later, and nothing is hydrating by then.
20621
20708
  *
20622
20709
  * Here rather than on each control.
20710
+ * - `selectionDirection`: The caret and the selection, under the platform's own names.
20711
+ *
20712
+ * **Published for the reason `Input` publishes them, and this element needs
20713
+ * them more.** The `<input>` is inside this shadow root, so a consumer has no
20714
+ * way to ask where the caret is: `querySelector` does not cross the boundary.
20715
+ * A grammar that completes what is being typed cannot work without it, because
20716
+ * "what is being typed" is the term the caret is in, and after writing a
20717
+ * completion back the caret has to be put after it or the next parse reads the
20718
+ * wrong term.
20719
+ *
20720
+ * Zero before there is a control, which is where an empty field's caret would
20721
+ * be anyway. (property only) (readonly)
20722
+ * - `selectionEnd`: Where the selection ends, which is the caret when nothing is selected. (property only) (readonly)
20723
+ * - `selectionStart`: Where the selection starts, which is the caret when nothing is selected. (property only) (readonly)
20623
20724
  * - `form`: The form this control belongs to, if any. (property only) (readonly)
20624
20725
  * - `labels`: Labels associated with this control. (property only) (readonly)
20625
20726
  * - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
@@ -20647,7 +20748,10 @@ export type CustomElements = {
20647
20748
  *
20648
20749
  * - `change`: Relayed from the input once the reader has finished.
20649
20750
  * - `helix-before-change`: Fired as the text changes, and cancelable.
20751
+ * - `helix-before-close`: Fired before the suggestions go, and cancelable.
20752
+ * - `helix-before-open`: Fired before the suggestions show, and cancelable.
20650
20753
  * - `input`: Announced once the text has moved.
20754
+ * - `toggle`: Says the panel moved, carrying `newState` and `oldState`.
20651
20755
  *
20652
20756
  * ## Slots
20653
20757
  *
@@ -20655,13 +20759,23 @@ export type CustomElements = {
20655
20759
  *
20656
20760
  * - `end`: After the text, for a count or a control.
20657
20761
  * - `start`: Before the text, for a search icon.
20762
+ * - `suggestions`: What could be typed next, as `Option`s, and `Option.Group`s of them.
20658
20763
  * - `tags`: What is already narrowing the results, as `Tag`s.
20659
20764
  *
20660
20765
  * ## Methods
20661
20766
  *
20662
20767
  * Methods that can be called to access component functionality.
20663
20768
  *
20769
+ * - `setSelectionRange(start: number, end: number, direction?: 'backward' | 'forward' | 'none') => void`: Moves the caret, or selects a run of the text.
20664
20770
  * - `focus(options?: FocusOptions) => void`: Puts the caret in the field, the way a label pointing at one would.
20771
+ * - `requestOpen(next: boolean) => boolean`: Requests an open-state change, and reports whether it was applied.
20772
+ *
20773
+ * Announces before it acts, so `preventDefault()` on the `helix-before-open` or
20774
+ * `helix-before-close` is a refusal and nothing moves. Nobody refusing means the
20775
+ * element opens or closes itself, whether or not a consumer declared
20776
+ * `open`: a declared property is a seed and a correction, and a consumer
20777
+ * who wants the decision for themselves refuses it and assigns their own.
20778
+ * - `toggleOpen() => boolean`: Requests the opposite of the current state.
20665
20779
  * - `requestValue(next: string) => boolean`: Requests a new value, and reports whether the request stood.
20666
20780
  *
20667
20781
  * Announces before it acts, so `preventDefault()` on the `helix-before-change` is
@@ -20724,6 +20838,7 @@ export type CustomElements = {
20724
20838
  * - `hint`: Help under the field, while nothing is wrong.
20725
20839
  * - `label`: The question, over the field.
20726
20840
  * - `rest`: How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.
20841
+ * - `surface`: The floating panel the suggestions sit in.
20727
20842
  * - `tags`: The row the tokens sit in.
20728
20843
  */
20729
20844
  "helix-headless-filter-input": Partial<
@@ -35007,6 +35122,20 @@ export type CustomElementsSolidJs = {
35007
35122
  * `helix-dismiss` its own remove control fires, so a consumer who handled the
35008
35123
  * pointer has already handled the keyboard.
35009
35124
  *
35125
+ * **It can suggest what to type next, and it never learns the grammar.** Slot
35126
+ * `helix-option` elements into `suggestions` and the field shows them, moves
35127
+ * through them with the arrows, and lets Enter press one; the option fires its
35128
+ * own `helix-select` and the consumer decides what choosing meant. Deciding
35129
+ * *what* to suggest is theirs too: keys before a colon and values after it are
35130
+ * the same panel with different contents, which is all a two-level filter is,
35131
+ * and the parse that tells them apart belongs where the domain does.
35132
+ *
35133
+ * **The panel opens when there is something in it and closes when there is
35134
+ * not**, rather than on a choice. A reader who picks a key wants its values
35135
+ * next, and closing on the choice would shut the panel between two renders. Use
35136
+ * `selectionStart` and `setSelectionRange` to write a completion back and put
35137
+ * the caret after it, or the next parse reads the wrong term.
35138
+ *
35010
35139
  * ## Attributes & Properties
35011
35140
  *
35012
35141
  * Component attributes and properties that can be applied to the element or by using JavaScript.
@@ -35021,6 +35150,19 @@ export type CustomElementsSolidJs = {
35021
35150
  * - `placeholder`: Shown while the field is empty.
35022
35151
  *
35023
35152
  * Not a label: it goes when typing starts.
35153
+ * - `open`: Whether the element is open.
35154
+ *
35155
+ * One property, not two, so `dialog.open` answers the question anyone
35156
+ * asking it meant. Writing always applies; `<helix-dialog open>` is the
35157
+ * same statement in markup.
35158
+ *
35159
+ * **Not reflected, and that is required rather than tidy.** Reflection
35160
+ * writes the resolved state back as an attribute, so an element that opened
35161
+ * itself would put `open` on its own host: cloned, serialized for
35162
+ * hydration, or diffed by a framework, it comes back holding whatever the
35163
+ * last reader did rather than what the author wrote. `:state(open)` carries
35164
+ * the resolved state for stylesheets, which is what the mirror is for.
35165
+ * - `default-open`/`defaultOpen`: Initial open state for uncontrolled use, where nothing declared one.
35024
35166
  * - `collapse-label`/`collapseLabel`: What the control says once everything is showing.
35025
35167
  * - `expandable`: Whether the count is a control that shows the rest.
35026
35168
  * - `expand-label`/`expandLabel`: What the collapsed count is called, since `+3` names nothing.
@@ -35060,6 +35202,20 @@ export type CustomElementsSolidJs = {
35060
35202
  * imperatively later, and nothing is hydrating by then.
35061
35203
  *
35062
35204
  * Here rather than on each control.
35205
+ * - `selectionDirection`: The caret and the selection, under the platform's own names.
35206
+ *
35207
+ * **Published for the reason `Input` publishes them, and this element needs
35208
+ * them more.** The `<input>` is inside this shadow root, so a consumer has no
35209
+ * way to ask where the caret is: `querySelector` does not cross the boundary.
35210
+ * A grammar that completes what is being typed cannot work without it, because
35211
+ * "what is being typed" is the term the caret is in, and after writing a
35212
+ * completion back the caret has to be put after it or the next parse reads the
35213
+ * wrong term.
35214
+ *
35215
+ * Zero before there is a control, which is where an empty field's caret would
35216
+ * be anyway. (property only) (readonly)
35217
+ * - `selectionEnd`: Where the selection ends, which is the caret when nothing is selected. (property only) (readonly)
35218
+ * - `selectionStart`: Where the selection starts, which is the caret when nothing is selected. (property only) (readonly)
35063
35219
  * - `form`: The form this control belongs to, if any. (property only) (readonly)
35064
35220
  * - `labels`: Labels associated with this control. (property only) (readonly)
35065
35221
  * - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
@@ -35087,7 +35243,10 @@ export type CustomElementsSolidJs = {
35087
35243
  *
35088
35244
  * - `change`: Relayed from the input once the reader has finished.
35089
35245
  * - `helix-before-change`: Fired as the text changes, and cancelable.
35246
+ * - `helix-before-close`: Fired before the suggestions go, and cancelable.
35247
+ * - `helix-before-open`: Fired before the suggestions show, and cancelable.
35090
35248
  * - `input`: Announced once the text has moved.
35249
+ * - `toggle`: Says the panel moved, carrying `newState` and `oldState`.
35091
35250
  *
35092
35251
  * ## Slots
35093
35252
  *
@@ -35095,13 +35254,23 @@ export type CustomElementsSolidJs = {
35095
35254
  *
35096
35255
  * - `end`: After the text, for a count or a control.
35097
35256
  * - `start`: Before the text, for a search icon.
35257
+ * - `suggestions`: What could be typed next, as `Option`s, and `Option.Group`s of them.
35098
35258
  * - `tags`: What is already narrowing the results, as `Tag`s.
35099
35259
  *
35100
35260
  * ## Methods
35101
35261
  *
35102
35262
  * Methods that can be called to access component functionality.
35103
35263
  *
35264
+ * - `setSelectionRange(start: number, end: number, direction?: 'backward' | 'forward' | 'none') => void`: Moves the caret, or selects a run of the text.
35104
35265
  * - `focus(options?: FocusOptions) => void`: Puts the caret in the field, the way a label pointing at one would.
35266
+ * - `requestOpen(next: boolean) => boolean`: Requests an open-state change, and reports whether it was applied.
35267
+ *
35268
+ * Announces before it acts, so `preventDefault()` on the `helix-before-open` or
35269
+ * `helix-before-close` is a refusal and nothing moves. Nobody refusing means the
35270
+ * element opens or closes itself, whether or not a consumer declared
35271
+ * `open`: a declared property is a seed and a correction, and a consumer
35272
+ * who wants the decision for themselves refuses it and assigns their own.
35273
+ * - `toggleOpen() => boolean`: Requests the opposite of the current state.
35105
35274
  * - `requestValue(next: string) => boolean`: Requests a new value, and reports whether the request stood.
35106
35275
  *
35107
35276
  * Announces before it acts, so `preventDefault()` on the `helix-before-change` is
@@ -35164,6 +35333,7 @@ export type CustomElementsSolidJs = {
35164
35333
  * - `hint`: Help under the field, while nothing is wrong.
35165
35334
  * - `label`: The question, over the field.
35166
35335
  * - `rest`: How many tokens did not fit. A `<button>` with `expandable`, a `<span>` without it.
35336
+ * - `surface`: The floating panel the suggestions sit in.
35167
35337
  * - `tags`: The row the tokens sit in.
35168
35338
  */
35169
35339
  "helix-headless-filter-input": Partial<