@fusion.dev/helix-headless 0.45.0 → 0.46.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.
@@ -10397,7 +10397,7 @@
10397
10397
  "type": {
10398
10398
  "text": "HelixSelectEvent"
10399
10399
  },
10400
- "description": "Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`.",
10400
+ "description": "Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`.",
10401
10401
  "name": "helix-select",
10402
10402
  "reactProp": "onhelix-select"
10403
10403
  }
@@ -11388,7 +11388,7 @@
11388
11388
  "type": {
11389
11389
  "text": "'failed' | 'sending' | 'sent' | 'streaming' | undefined"
11390
11390
  },
11391
- "description": "What has happened to it.\n\nReflected, because every one of the four is a visual state and a stylesheet\nhas no other way to know. Absent is a settled message with nothing to\nreport, which is what most of a transcript is, and it is the absence of the\nattribute rather than a value of it: there is no initializer, so nothing is\nreflected until something happens.",
11391
+ "description": "What has happened to it.\n\nReflected, because every one of the four is a visual state and a stylesheet\nhas no other way to know. Absent is a settled message with nothing to\nreport, which is what most of a transcript is, and it is the absence of the\nattribute rather than a value of it: there is no initializer, so nothing is\nreflected until something happens.\n\n**The turn writes this once, on a retry nobody refused**, and `handleRetry`\nholds the reason. Everything else that moves it is the consumer's.",
11392
11392
  "attribute": "state",
11393
11393
  "reflects": true
11394
11394
  },
@@ -11477,7 +11477,7 @@
11477
11477
  "name": "handleRetry",
11478
11478
  "privacy": "private",
11479
11479
  "readonly": true,
11480
- "description": "Retry is `helix-send` again, from the message rather than the composer.\n\n**Not an event of its own**, because a retry is the same request: the reader\nis offering what they wrote, and the only difference is which element is\noffering it. A listener that wants to tell them apart has `event.target`,\nwhich is the message on a retry and the composer on a first send, and that\nis a distinction the DOM already makes without a second name for it."
11480
+ "description": "Retry is `helix-send` again, from the message rather than the composer.\n\n**Not an event of its own**, because a retry is the same request: the reader\nis offering what they wrote, and the only difference is which element is\noffering it. A listener that wants to tell them apart has `event.target`,\nwhich is the message on a retry and the composer on a first send, and that\nis a distinction the DOM already makes without a second name for it.\n\n**Through `requestChange`, and the turn is what moves.** `helix-send` is\ncancelable because the composer clears only when nobody refused, and this\ndispatch had nothing behind it: the reader pressed the control, the turn sat\nat \"Not sent\" with the same control still on it, and `preventDefault()`\nrefused a decision nobody here was about to take. A retry that stands is a\nturn on its way, which is what `sending` means, so that is the default\naction and refusing it leaves the failure exactly where it was. A consumer\nwho wants something else refuses and assigns `state` themselves, which is\nhow control is taken from every other stateful element in the library."
11481
11481
  },
11482
11482
  {
11483
11483
  "kind": "field",
@@ -11809,7 +11809,7 @@
11809
11809
  "type": {
11810
11810
  "text": "HelixSendEvent"
11811
11811
  },
11812
- "description": "Fired when the reader asks to send a failed message again, and cancelable.",
11812
+ "description": "Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were.",
11813
11813
  "name": "helix-send",
11814
11814
  "reactProp": "onhelix-send"
11815
11815
  }
@@ -11870,7 +11870,7 @@
11870
11870
  "type": {
11871
11871
  "text": "'failed' | 'sending' | 'sent' | 'streaming' | undefined"
11872
11872
  },
11873
- "description": "What has happened to it.\n\nReflected, because every one of the four is a visual state and a stylesheet\nhas no other way to know. Absent is a settled message with nothing to\nreport, which is what most of a transcript is, and it is the absence of the\nattribute rather than a value of it: there is no initializer, so nothing is\nreflected until something happens.",
11873
+ "description": "What has happened to it.\n\nReflected, because every one of the four is a visual state and a stylesheet\nhas no other way to know. Absent is a settled message with nothing to\nreport, which is what most of a transcript is, and it is the absence of the\nattribute rather than a value of it: there is no initializer, so nothing is\nreflected until something happens.\n\n**The turn writes this once, on a retry nobody refused**, and `handleRetry`\nholds the reason. Everything else that moves it is the consumer's.",
11874
11874
  "fieldName": "state"
11875
11875
  }
11876
11876
  ],
@@ -19257,6 +19257,17 @@
19257
19257
  "readonly": true,
19258
19258
  "description": "Dragging in the square.\n\nThe two ranges are the state, so this reads a point and writes to them\nrather than keeping a position of its own. Pointer capture, so a drag that\nleaves the square keeps going: leaving and coming back is how a reader\nreaches a corner, and without it the handle stops wherever the pointer left."
19259
19259
  },
19260
+ {
19261
+ "kind": "method",
19262
+ "name": "sync",
19263
+ "privacy": "private",
19264
+ "return": {
19265
+ "type": {
19266
+ "text": "void"
19267
+ }
19268
+ },
19269
+ "description": "Reports the color to the form.\n\n**The one thing a form-associated element owes, and this element owed it\nfrom the start without paying.** It extends `HelixFormElement`, takes\n`MixinControllerValue` and carries `name`, so a consumer writing\n`<helix-color-picker name=\"brand\">` inside a form has every reason to\nexpect an entry, and the form carried none: no error, nothing in the\nmarkup to look at, and a field silently missing from the submission. Its\ntwo siblings in the same composition position, Color Input and Slider,\nboth report.\n\n`null` rather than an empty string where there is no color, which is the\ndifference between a control that submits nothing and one that submits a\nblank. There is no `setValidity` beside it: this element does not take\n`MixinField`, and every validity member of `ElementInternals` is the\nconsumer's to reach through the field that wraps it."
19270
+ },
19260
19271
  {
19261
19272
  "kind": "method",
19262
19273
  "name": "focus",
@@ -36529,9 +36540,63 @@
36529
36540
  "text": "removeAt(at: number) => void"
36530
36541
  }
36531
36542
  },
36543
+ {
36544
+ "kind": "field",
36545
+ "name": "committed",
36546
+ "type": {
36547
+ "text": "File[]"
36548
+ },
36549
+ "privacy": "private",
36550
+ "default": "[]",
36551
+ "description": "The files this element last stood behind, so a refusal has somewhere to go.\n\nThe picker and a drop both write the input before anything here runs, so\nthe list being replaced is gone by the time the request is announced.\nNothing else records it: `files` reads the control, which is already the\nnew answer."
36552
+ },
36532
36553
  {
36533
36554
  "kind": "method",
36534
- "name": "announce",
36555
+ "name": "commit",
36556
+ "privacy": "private",
36557
+ "return": {
36558
+ "type": {
36559
+ "text": "void"
36560
+ }
36561
+ },
36562
+ "parameters": [
36563
+ {
36564
+ "name": "next",
36565
+ "type": {
36566
+ "text": "File[]"
36567
+ }
36568
+ }
36569
+ ],
36570
+ "description": "Asks for a set of files, and puts them in unless a listener refuses.\n\n**The request was announced after the fact and nothing read it.** This\nelement dispatched `helix-before-change` from `announce`, after the input\nhad taken the files and after `setFormValue` had reported them, and\ndiscarded what the dispatch returned: an `@event` line promising that\nrefusing it left nothing moved, over a veto with no reader. The other\ndirection of that same fault is what `requestChange` exists to prevent, so\nthis goes through it like every other value control.\n\n**A refusal puts the input back rather than stopping an assignment**, which\nis the shape a file input forces. Two of the four paths in are the browser\nwriting `control.files` before this element hears anything, so there is no\nassignment left to skip and the honest answer is to restore the list the\nelement last stood behind."
36571
+ },
36572
+ {
36573
+ "kind": "method",
36574
+ "name": "put",
36575
+ "privacy": "private",
36576
+ "return": {
36577
+ "type": {
36578
+ "text": "void"
36579
+ }
36580
+ },
36581
+ "parameters": [
36582
+ {
36583
+ "name": "control",
36584
+ "type": {
36585
+ "text": "HTMLInputElement"
36586
+ }
36587
+ },
36588
+ {
36589
+ "name": "files",
36590
+ "type": {
36591
+ "text": "File[]"
36592
+ }
36593
+ }
36594
+ ],
36595
+ "description": "Writes a list onto the input, which is the only way a `FileList` is built.\n\n`value = ''` for the empty case rather than an empty `DataTransfer`,\nbecause emptying is the one write a control accepts everywhere and a\nharness without `DataTransfer` still has to be able to clear."
36596
+ },
36597
+ {
36598
+ "kind": "method",
36599
+ "name": "settle",
36535
36600
  "privacy": "private",
36536
36601
  "return": {
36537
36602
  "type": {
@@ -36611,7 +36676,7 @@
36611
36676
  "text": "void"
36612
36677
  }
36613
36678
  },
36614
- "description": "Empties it, which is what a form reset means for a file input.",
36679
+ "description": "Empties it, which is what a form reset means for a file input.\n\nNot through `commit`, and that is the point of the split: a reset is the\nform's decision and there is nothing here for a listener to refuse. What it\nowes is the report and the platform's own announcement, which is what\n`settle` is.",
36615
36680
  "type": {
36616
36681
  "text": "formResetCallback() => void"
36617
36682
  }
@@ -37353,7 +37418,7 @@
37353
37418
  "type": {
37354
37419
  "text": "HelixBeforeChangeEvent"
37355
37420
  },
37356
- "description": "Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves.",
37421
+ "description": "Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included.",
37357
37422
  "name": "helix-before-change",
37358
37423
  "reactProp": "onhelix-before-change"
37359
37424
  },
@@ -70356,7 +70421,7 @@
70356
70421
  "name": "handleSelection",
70357
70422
  "privacy": "private",
70358
70423
  "readonly": true,
70359
- "description": "Announces before it applies, so a listener can refuse the change.\n\nThe item has not moved by the time this runs, unlike a native input, so\nrefusing is simply not writing the value: there is nothing to put back."
70424
+ "description": "Announces before it applies, so a listener can refuse the change.\n\nThe item has not moved by the time this runs, unlike a native input, so\nrefusing is simply not writing the value: there is nothing to put back.\n\n**The radio's own `helix-select` is a request, and the argument that it was\na notification is the one this settles.** That argument ran: the group's\nveto is `helix-before-change` a step later, so the item could be read as\nannouncing a choice the group then decides about. What it leaves out is who\ncan reach which event. `helix-before-change` is dispatched on the group, so\nrefusing it needs a listener on the group, and a consumer ruling out one\nanswer binds to that answer. Two gates in two positions is the shape, and\nthe item's is the one that was doing nothing."
70360
70425
  },
70361
70426
  {
70362
70427
  "kind": "method",
@@ -27,7 +27,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
27
27
  *
28
28
  * @slot - The slides.
29
29
  *
30
- * @event {HelixSelectEvent} helix-select - Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`.
30
+ * @event {HelixSelectEvent} helix-select - Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`.
31
31
  *
32
32
  * @csspart controls - The row of controls.
33
33
  * @csspart indicator - One of them, and a way to reach that slide.
@@ -60,9 +60,19 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
60
60
  if (nearest === this.index) {
61
61
  return;
62
62
  }
63
- this.indexState = nearest;
64
- this.requestUpdate('index');
65
- this.dispatchEvent(new HelixSelectEvent({ value: String(nearest) }));
63
+ const applied = requestChange({
64
+ apply: (value) => {
65
+ const previous = this.index;
66
+ this.indexState = value;
67
+ this.requestUpdate('index', previous);
68
+ },
69
+ event: (value) => new HelixSelectEvent({ value: String(value) }),
70
+ host: this,
71
+ next: nearest,
72
+ });
73
+ if (!applied) {
74
+ this.scrollToIndex();
75
+ }
66
76
  };
67
77
  this.scrollFrame = 0;
68
78
  this.stopped = false;
@@ -35,7 +35,7 @@ export declare const FAILED_LABEL = "Not sent";
35
35
  * indicator, so slotting nothing is what this has always done.
36
36
  * @slot sources - What it was said from: citations, documents, a retrieved page.
37
37
  *
38
- * @event {HelixSendEvent} helix-send - Fired when the reader asks to send a failed message again, and cancelable.
38
+ * @event {HelixSendEvent} helix-send - Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were.
39
39
  *
40
40
  * @csspart actions - The controls offered on the turn, beside its time.
41
41
  * @csspart avatar - The leading slot's box.
@@ -91,6 +91,9 @@ export declare class HelixHeadlessChatMessage extends HelixElement {
91
91
  * report, which is what most of a transcript is, and it is the absence of the
92
92
  * attribute rather than a value of it: there is no initializer, so nothing is
93
93
  * reflected until something happens.
94
+ *
95
+ * **The turn writes this once, on a retry nobody refused**, and `handleRetry`
96
+ * holds the reason. Everything else that moves it is the consumer's.
94
97
  */
95
98
  state?: 'failed' | 'sending' | 'sent' | 'streaming';
96
99
  /**
@@ -180,6 +183,16 @@ export declare class HelixHeadlessChatMessage extends HelixElement {
180
183
  * offering it. A listener that wants to tell them apart has `event.target`,
181
184
  * which is the message on a retry and the composer on a first send, and that
182
185
  * is a distinction the DOM already makes without a second name for it.
186
+ *
187
+ * **Through `requestChange`, and the turn is what moves.** `helix-send` is
188
+ * cancelable because the composer clears only when nobody refused, and this
189
+ * dispatch had nothing behind it: the reader pressed the control, the turn sat
190
+ * at "Not sent" with the same control still on it, and `preventDefault()`
191
+ * refused a decision nobody here was about to take. A retry that stands is a
192
+ * turn on its way, which is what `sending` means, so that is the default
193
+ * action and refusing it leaves the failure exactly where it was. A consumer
194
+ * who wants something else refuses and assigns `state` themselves, which is
195
+ * how control is taken from every other stateful element in the library.
183
196
  */
184
197
  private readonly handleRetry;
185
198
  }
@@ -4,7 +4,7 @@ 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 { customElement, HelixElement, HelixSendEvent, hostAria, relativeTime, slotFilled, TONE_MARKS, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, HelixSendEvent, hostAria, relativeTime, requestChange, slotFilled, TONE_MARKS, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -24,7 +24,14 @@ let HelixHeadlessChatMessage = class HelixHeadlessChatMessage extends HelixEleme
24
24
  };
25
25
  this.handleRetry = (event) => {
26
26
  event.stopPropagation();
27
- this.dispatchEvent(new HelixSendEvent({ value: this.textContent?.trim() ?? '' }));
27
+ requestChange({
28
+ apply: (next) => {
29
+ this.state = next;
30
+ },
31
+ event: () => new HelixSendEvent({ value: this.textContent?.trim() ?? '' }),
32
+ host: this,
33
+ next: 'sending',
34
+ });
28
35
  };
29
36
  }
30
37
  get moment() {
@@ -23,6 +23,9 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
23
23
  return;
24
24
  }
25
25
  event.stopImmediatePropagation();
26
+ if (event.defaultPrevented) {
27
+ return;
28
+ }
26
29
  const wanted = event.detail.checked;
27
30
  const next = this.boxes
28
31
  .filter((box) => (box === changed ? wanted : box.checked))
@@ -223,6 +223,25 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
223
223
  */
224
224
  private readonly handleGridPointer;
225
225
  protected firstUpdated(changed: import('lit').PropertyValues): void;
226
+ /**
227
+ * Reports the color to the form.
228
+ *
229
+ * **The one thing a form-associated element owes, and this element owed it
230
+ * from the start without paying.** It extends `HelixFormElement`, takes
231
+ * `MixinControllerValue` and carries `name`, so a consumer writing
232
+ * `<helix-color-picker name="brand">` inside a form has every reason to
233
+ * expect an entry, and the form carried none: no error, nothing in the
234
+ * markup to look at, and a field silently missing from the submission. Its
235
+ * two siblings in the same composition position, Color Input and Slider,
236
+ * both report.
237
+ *
238
+ * `null` rather than an empty string where there is no color, which is the
239
+ * difference between a control that submits nothing and one that submits a
240
+ * blank. There is no `setValidity` beside it: this element does not take
241
+ * `MixinField`, and every validity member of `ElementInternals` is the
242
+ * consumer's to reach through the field that wraps it.
243
+ */
244
+ private sync;
226
245
  protected updated(changed: import('lit').PropertyValues): void;
227
246
  /** Puts the keyboard on the square, which is the control a reader wants. */
228
247
  focus(options?: FocusOptions): void;
@@ -106,8 +106,12 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinContr
106
106
  super.firstUpdated(changed);
107
107
  this.finishHydration();
108
108
  }
109
+ sync() {
110
+ this.internals.setFormValue(this.value === '' ? null : this.value);
111
+ }
109
112
  updated(changed) {
110
113
  super.updated(changed);
114
+ this.sync();
111
115
  if (changed.has('value')) {
112
116
  if (parseColor(this.value)) {
113
117
  this.settled = this.value;
@@ -35,7 +35,11 @@ let HelixHeadlessDrilldown = class HelixHeadlessDrilldown extends MixinCollectio
35
35
  }
36
36
  if (this.drilldown.hasPage(item)) {
37
37
  event.stopPropagation();
38
- this.enter(item);
38
+ queueMicrotask(() => {
39
+ if (!event.defaultPrevented) {
40
+ this.enter(item);
41
+ }
42
+ });
39
43
  }
40
44
  };
41
45
  this.handleKeydown = (event) => {
@@ -28,7 +28,7 @@ declare const HelixHeadlessFileInput_base: (new (...args: any[]) => import("@fus
28
28
  * @slot trigger - What the reader presses to open the picker.
29
29
  *
30
30
  * @event {Event} change - Announced once the value has moved.
31
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves.
31
+ * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included.
32
32
  * @event {Event} input - Announced once the value has moved.
33
33
  *
34
34
  * @csspart control - The file input itself, which carries the label and validity.
@@ -129,8 +129,43 @@ export declare class HelixHeadlessFileInput extends HelixHeadlessFileInput_base
129
129
  * picker come back out the same way.
130
130
  */
131
131
  removeAt(at: number): void;
132
+ /**
133
+ * The files this element last stood behind, so a refusal has somewhere to go.
134
+ *
135
+ * The picker and a drop both write the input before anything here runs, so
136
+ * the list being replaced is gone by the time the request is announced.
137
+ * Nothing else records it: `files` reads the control, which is already the
138
+ * new answer.
139
+ */
140
+ private committed;
141
+ /**
142
+ * Asks for a set of files, and puts them in unless a listener refuses.
143
+ *
144
+ * **The request was announced after the fact and nothing read it.** This
145
+ * element dispatched `helix-before-change` from `announce`, after the input
146
+ * had taken the files and after `setFormValue` had reported them, and
147
+ * discarded what the dispatch returned: an `@event` line promising that
148
+ * refusing it left nothing moved, over a veto with no reader. The other
149
+ * direction of that same fault is what `requestChange` exists to prevent, so
150
+ * this goes through it like every other value control.
151
+ *
152
+ * **A refusal puts the input back rather than stopping an assignment**, which
153
+ * is the shape a file input forces. Two of the four paths in are the browser
154
+ * writing `control.files` before this element hears anything, so there is no
155
+ * assignment left to skip and the honest answer is to restore the list the
156
+ * element last stood behind.
157
+ */
158
+ private commit;
159
+ /**
160
+ * Writes a list onto the input, which is the only way a `FileList` is built.
161
+ *
162
+ * `value = ''` for the empty case rather than an empty `DataTransfer`,
163
+ * because emptying is the one write a control accepts everywhere and a
164
+ * harness without `DataTransfer` still has to be able to clear.
165
+ */
166
+ private put;
132
167
  /** Says what is now chosen, by name, and puts it in the form. */
133
- private announce;
168
+ private settle;
134
169
  /**
135
170
  * Reports the files and the validity the host stands behind.
136
171
  *
@@ -148,7 +183,14 @@ export declare class HelixHeadlessFileInput extends HelixHeadlessFileInput_base
148
183
  protected firstUpdated(changed: PropertyValues): void;
149
184
  /** Puts focus on the input, the way a label pointing at one would. */
150
185
  focus(options?: FocusOptions): void;
151
- /** Empties it, which is what a form reset means for a file input. */
186
+ /**
187
+ * Empties it, which is what a form reset means for a file input.
188
+ *
189
+ * Not through `commit`, and that is the point of the split: a reset is the
190
+ * form's decision and there is nothing here for a listener to refuse. What it
191
+ * owes is the report and the platform's own announcement, which is what
192
+ * `settle` is.
193
+ */
152
194
  formResetCallback(): void;
153
195
  /**
154
196
  * What is known about a chosen file, as tags, behind a seam the styled tier
@@ -4,7 +4,7 @@ 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, HelixBeforeChangeEvent, HelixFormElement, MixinField, restoreValue, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, drawIcon, HelixBeforeChangeEvent, HelixFormElement, MixinField, requestChange, restoreValue, } from '@fusion.dev/helix-core';
8
8
  import { trash } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -44,9 +44,10 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
44
44
  this.removeLabel = 'Remove';
45
45
  this.triggerLabel = 'Choose files';
46
46
  this.dropping = false;
47
+ this.committed = [];
47
48
  this.handleChange = (event) => {
48
49
  event.stopPropagation();
49
- this.announce();
50
+ this.commit(this.files);
50
51
  };
51
52
  this.handleDragOver = (event) => {
52
53
  if (this.isDisabled) {
@@ -69,12 +70,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
69
70
  if (dropped.length === 0) {
70
71
  return;
71
72
  }
72
- const keeping = new DataTransfer();
73
- for (const file of this.multiple ? [...this.files, ...dropped] : dropped.slice(-1)) {
74
- keeping.items.add(file);
75
- }
76
- control.files = keeping.files;
77
- this.announce();
73
+ this.commit(this.multiple ? [...this.files, ...dropped] : dropped.slice(-1));
78
74
  };
79
75
  this.handleRemove = (event) => {
80
76
  const at = Number(event.currentTarget.dataset.at);
@@ -90,33 +86,48 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
90
86
  return this.partNamed('control');
91
87
  }
92
88
  clear() {
89
+ this.commit([]);
90
+ }
91
+ removeAt(at) {
92
+ this.commit(this.files.filter((_, index) => index !== at));
93
+ }
94
+ commit(next) {
93
95
  const control = this.control;
94
96
  if (!control) {
95
97
  return;
96
98
  }
97
- control.value = '';
98
- this.announce();
99
+ const previous = this.committed;
100
+ const applied = requestChange({
101
+ apply: (files) => {
102
+ this.put(control, files);
103
+ this.settle();
104
+ },
105
+ event: (files) => new HelixBeforeChangeEvent({ value: files.map((file) => file.name).join(', ') }),
106
+ host: this,
107
+ next,
108
+ });
109
+ if (!applied) {
110
+ this.put(control, previous);
111
+ }
99
112
  }
100
- removeAt(at) {
101
- const control = this.control;
102
- if (!control || typeof DataTransfer === 'undefined') {
113
+ put(control, files) {
114
+ if (files.length === 0) {
115
+ control.value = '';
103
116
  return;
104
117
  }
105
- const keeping = new DataTransfer();
106
- for (const [index, file] of this.files.entries()) {
107
- if (index !== at) {
108
- keeping.items.add(file);
109
- }
118
+ if (typeof DataTransfer === 'undefined') {
119
+ return;
120
+ }
121
+ const holding = new DataTransfer();
122
+ for (const file of files) {
123
+ holding.items.add(file);
110
124
  }
111
- control.files = keeping.files;
112
- this.announce();
125
+ control.files = holding.files;
113
126
  }
114
- announce() {
127
+ settle() {
128
+ this.committed = this.files;
115
129
  this.sync();
116
130
  this.requestUpdate();
117
- this.dispatchEvent(new HelixBeforeChangeEvent({
118
- value: this.files.map((file) => file.name).join(', '),
119
- }));
120
131
  this.announceInput();
121
132
  this.announceChange();
122
133
  }
@@ -137,6 +148,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
137
148
  }
138
149
  firstUpdated(changed) {
139
150
  super.firstUpdated(changed);
151
+ this.committed = this.files;
140
152
  this.sync();
141
153
  }
142
154
  focus(options) {
@@ -146,7 +158,7 @@ let HelixHeadlessFileInput = class HelixHeadlessFileInput extends MixinField(Hel
146
158
  if (this.control) {
147
159
  restoreValue(this.control, '');
148
160
  }
149
- this.announce();
161
+ this.settle();
150
162
  }
151
163
  renderMeta(file) {
152
164
  return html `<${this.tag('tag')} part="size"
@@ -77,6 +77,9 @@ let HelixHeadlessListbox = class HelixHeadlessListbox extends MixinCollection(Mi
77
77
  });
78
78
  this.handleSelection = (event) => {
79
79
  event.stopPropagation();
80
+ if (event.defaultPrevented) {
81
+ return;
82
+ }
80
83
  this.activeValue = event.detail.value;
81
84
  this.choose(event.detail.value);
82
85
  };
@@ -67,6 +67,15 @@ export declare class HelixHeadlessRadioGroup extends HelixHeadlessRadioGroup_bas
67
67
  *
68
68
  * The item has not moved by the time this runs, unlike a native input, so
69
69
  * refusing is simply not writing the value: there is nothing to put back.
70
+ *
71
+ * **The radio's own `helix-select` is a request, and the argument that it was
72
+ * a notification is the one this settles.** That argument ran: the group's
73
+ * veto is `helix-before-change` a step later, so the item could be read as
74
+ * announcing a choice the group then decides about. What it leaves out is who
75
+ * can reach which event. `helix-before-change` is dispatched on the group, so
76
+ * refusing it needs a listener on the group, and a consumer ruling out one
77
+ * answer binds to that answer. Two gates in two positions is the shape, and
78
+ * the item's is the one that was doing nothing.
70
79
  */
71
80
  private readonly handleSelection;
72
81
  /** Writes the answer onto the items, and nothing onto the host. */
@@ -17,6 +17,9 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
17
17
  });
18
18
  this.handleSelection = (event) => {
19
19
  event.stopPropagation();
20
+ if (event.defaultPrevented) {
21
+ return;
22
+ }
20
23
  const next = event.detail.value;
21
24
  if (next === this.value) {
22
25
  return;
@@ -12,6 +12,9 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
12
12
  super(...arguments);
13
13
  this.label = 'Steps';
14
14
  this.handleSelection = (event) => {
15
+ if (event.defaultPrevented) {
16
+ return;
17
+ }
15
18
  this.requestActive(event.detail.value);
16
19
  };
17
20
  this.handleSlotChange = () => {
@@ -26,6 +26,9 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
26
26
  this.label = 'Tabs';
27
27
  this.manual = false;
28
28
  this.handleSelection = (event) => {
29
+ if (event.defaultPrevented) {
30
+ return;
31
+ }
29
32
  this.requestActive(event.detail.value);
30
33
  };
31
34
  this.handleSlotChange = () => {
@@ -22,6 +22,9 @@ let HelixHeadlessToggleGroup = class HelixHeadlessToggleGroup extends MixinContr
22
22
  items: () => this.items,
23
23
  });
24
24
  this.handleSelection = (event) => {
25
+ if (event.defaultPrevented) {
26
+ return;
27
+ }
25
28
  this.requestValue(event.detail.value);
26
29
  };
27
30
  }
@@ -23,7 +23,9 @@ let HelixHeadlessTree = class HelixHeadlessTree extends MixinCollection(MixinCon
23
23
  super(...arguments);
24
24
  this.label = 'Tree';
25
25
  this.handleSelection = (event) => {
26
- this.requestActive(event.detail.value);
26
+ if (!event.defaultPrevented) {
27
+ this.requestActive(event.detail.value);
28
+ }
27
29
  this.syncItems();
28
30
  };
29
31
  this.handleKeydown = (event) => {
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.45.0",
4
+ "version": "0.46.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -46,14 +46,14 @@
46
46
  },
47
47
  "bugs": "https://github.com/project-fusion/fusion/issues",
48
48
  "dependencies": {
49
- "@fusion.dev/helix-core": "0.45.0",
50
- "@fusion.dev/helix-icons": "0.45.0",
49
+ "@fusion.dev/helix-core": "0.46.0",
50
+ "@fusion.dev/helix-icons": "0.46.0",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@custom-elements-manifest/analyzer": "^0.11.0",
55
- "@fusion.dev/helix-cem": "0.45.0",
56
- "@fusion.dev/helix-testing": "0.45.0",
55
+ "@fusion.dev/helix-cem": "0.46.0",
56
+ "@fusion.dev/helix-testing": "0.46.0",
57
57
  "@fusion.dev/scripts": "0.0.0-dev",
58
58
  "@happy-dom/global-registrator": "^20.0.0",
59
59
  "@lit-labs/ssr": "^4.1.0",
package/types/jsx.d.ts CHANGED
@@ -1861,7 +1861,7 @@ server markup never had, which React reports as a mismatch. The one-across
1861
1861
  case is the absent attribute, and the styles read it that way. */
1862
1862
  perView?: (HelixHeadlessCarousel["perView"]) | undefined;
1863
1863
 
1864
- /** Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`. */
1864
+ /** Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`. */
1865
1865
  "onhelix-select"?: ((e: HelixHeadlessCarouselHelixSelectElementEvent) => void) | undefined;
1866
1866
  };
1867
1867
 
@@ -1961,7 +1961,7 @@ its attribute on upgrade, and the host then carries something a consumer's
1961
1961
  server markup never had, which React reports as a mismatch. The one-across
1962
1962
  case is the absent attribute, and the styles read it that way. */
1963
1963
  "prop:perView"?: (HelixHeadlessCarousel["perView"]) | undefined;
1964
- /** Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`. */
1964
+ /** Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`. */
1965
1965
  "on:helix-select"?: ((e: HelixHeadlessCarouselHelixSelectElementEvent) => void) | undefined;
1966
1966
 
1967
1967
  /** Set the innerHTML of the element */
@@ -2097,10 +2097,13 @@ Reflected, because every one of the four is a visual state and a stylesheet
2097
2097
  has no other way to know. Absent is a settled message with nothing to
2098
2098
  report, which is what most of a transcript is, and it is the absence of the
2099
2099
  attribute rather than a value of it: there is no initializer, so nothing is
2100
- reflected until something happens. */
2100
+ reflected until something happens.
2101
+
2102
+ **The turn writes this once, on a retry nobody refused**, and `handleRetry`
2103
+ holds the reason. Everything else that moves it is the consumer's. */
2101
2104
  state?: (HelixHeadlessChatMessage["state"]) | undefined;
2102
2105
 
2103
- /** Fired when the reader asks to send a failed message again, and cancelable. */
2106
+ /** Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were. */
2104
2107
  "onhelix-send"?: ((e: HelixHeadlessChatMessageHelixSendElementEvent) => void) | undefined;
2105
2108
  };
2106
2109
 
@@ -2137,9 +2140,12 @@ Reflected, because every one of the four is a visual state and a stylesheet
2137
2140
  has no other way to know. Absent is a settled message with nothing to
2138
2141
  report, which is what most of a transcript is, and it is the absence of the
2139
2142
  attribute rather than a value of it: there is no initializer, so nothing is
2140
- reflected until something happens. */
2143
+ reflected until something happens.
2144
+
2145
+ **The turn writes this once, on a retry nobody refused**, and `handleRetry`
2146
+ holds the reason. Everything else that moves it is the consumer's. */
2141
2147
  "prop:state"?: (HelixHeadlessChatMessage["state"]) | undefined;
2142
- /** Fired when the reader asks to send a failed message again, and cancelable. */
2148
+ /** Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were. */
2143
2149
  "on:helix-send"?: ((e: HelixHeadlessChatMessageHelixSendElementEvent) => void) | undefined;
2144
2150
 
2145
2151
  /** Set the innerHTML of the element */
@@ -6195,7 +6201,7 @@ Here rather than on each control. */
6195
6201
 
6196
6202
  /** Announced once the value has moved. */
6197
6203
  onchange?: ((e: HelixHeadlessFileInputElementEvent) => void) | undefined;
6198
- /** Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves. */
6204
+ /** Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included. */
6199
6205
  "onhelix-before-change"?: ((
6200
6206
  e: HelixHeadlessFileInputHelixBeforeChangeElementEvent,
6201
6207
  ) => void) | undefined;
@@ -6258,7 +6264,7 @@ Here rather than on each control. */
6258
6264
  "prop:name"?: (HelixHeadlessFileInput["name"]) | undefined;
6259
6265
  /** Announced once the value has moved. */
6260
6266
  "on:change"?: ((e: HelixHeadlessFileInputElementEvent) => void) | undefined;
6261
- /** Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves. */
6267
+ /** Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included. */
6262
6268
  "on:helix-before-change"?: ((
6263
6269
  e: HelixHeadlessFileInputHelixBeforeChangeElementEvent,
6264
6270
  ) => void) | undefined;
@@ -19178,7 +19184,7 @@ export type CustomElements = {
19178
19184
  *
19179
19185
  * Events that will be emitted by the component.
19180
19186
  *
19181
- * - `helix-select`: Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`.
19187
+ * - `helix-select`: Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`.
19182
19188
  *
19183
19189
  * ## Slots
19184
19190
  *
@@ -19366,6 +19372,9 @@ export type CustomElements = {
19366
19372
  * report, which is what most of a transcript is, and it is the absence of the
19367
19373
  * attribute rather than a value of it: there is no initializer, so nothing is
19368
19374
  * reflected until something happens.
19375
+ *
19376
+ * **The turn writes this once, on a retry nobody refused**, and `handleRetry`
19377
+ * holds the reason. Everything else that moves it is the consumer's.
19369
19378
  * - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
19370
19379
  *
19371
19380
  * **Named `whenSettled` because `settled` is taken four times over**, by
@@ -19391,7 +19400,7 @@ export type CustomElements = {
19391
19400
  *
19392
19401
  * Events that will be emitted by the component.
19393
19402
  *
19394
- * - `helix-send`: Fired when the reader asks to send a failed message again, and cancelable.
19403
+ * - `helix-send`: Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were.
19395
19404
  *
19396
19405
  * ## Slots
19397
19406
  *
@@ -23238,7 +23247,7 @@ export type CustomElements = {
23238
23247
  * Events that will be emitted by the component.
23239
23248
  *
23240
23249
  * - `change`: Announced once the value has moved.
23241
- * - `helix-before-change`: Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves.
23250
+ * - `helix-before-change`: Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included.
23242
23251
  * - `input`: Announced once the value has moved.
23243
23252
  *
23244
23253
  * ## Slots
@@ -23269,6 +23278,11 @@ export type CustomElements = {
23269
23278
  * picker come back out the same way.
23270
23279
  * - `focus(options?: FocusOptions) => void`: Puts focus on the input, the way a label pointing at one would.
23271
23280
  * - `formResetCallback() => void`: Empties it, which is what a form reset means for a file input.
23281
+ *
23282
+ * Not through `commit`, and that is the point of the split: a reset is the
23283
+ * form's decision and there is nothing here for a listener to refuse. What it
23284
+ * owes is the report and the platform's own announcement, which is what
23285
+ * `settle` is.
23272
23286
  * - `formDisabledCallback(disabled: boolean) => void`: The platform's notice that an ancestor fieldset changed.
23273
23287
  *
23274
23288
  * **The argument is the effective state, and taking it whole is what latched
@@ -36912,7 +36926,7 @@ export type CustomElementsSolidJs = {
36912
36926
  *
36913
36927
  * Events that will be emitted by the component.
36914
36928
  *
36915
- * - `helix-select`: Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`.
36929
+ * - `helix-select`: Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`.
36916
36930
  *
36917
36931
  * ## Slots
36918
36932
  *
@@ -37104,6 +37118,9 @@ export type CustomElementsSolidJs = {
37104
37118
  * report, which is what most of a transcript is, and it is the absence of the
37105
37119
  * attribute rather than a value of it: there is no initializer, so nothing is
37106
37120
  * reflected until something happens.
37121
+ *
37122
+ * **The turn writes this once, on a retry nobody refused**, and `handleRetry`
37123
+ * holds the reason. Everything else that moves it is the consumer's.
37107
37124
  * - `whenSettled`: Resolves a frame after the first update, when transitions are switched on.
37108
37125
  *
37109
37126
  * **Named `whenSettled` because `settled` is taken four times over**, by
@@ -37129,7 +37146,7 @@ export type CustomElementsSolidJs = {
37129
37146
  *
37130
37147
  * Events that will be emitted by the component.
37131
37148
  *
37132
- * - `helix-send`: Fired when the reader asks to send a failed message again, and cancelable.
37149
+ * - `helix-send`: Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were.
37133
37150
  *
37134
37151
  * ## Slots
37135
37152
  *
@@ -41025,7 +41042,7 @@ export type CustomElementsSolidJs = {
41025
41042
  * Events that will be emitted by the component.
41026
41043
  *
41027
41044
  * - `change`: Announced once the value has moved.
41028
- * - `helix-before-change`: Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves.
41045
+ * - `helix-before-change`: Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included.
41029
41046
  * - `input`: Announced once the value has moved.
41030
41047
  *
41031
41048
  * ## Slots
@@ -41056,6 +41073,11 @@ export type CustomElementsSolidJs = {
41056
41073
  * picker come back out the same way.
41057
41074
  * - `focus(options?: FocusOptions) => void`: Puts focus on the input, the way a label pointing at one would.
41058
41075
  * - `formResetCallback() => void`: Empties it, which is what a form reset means for a file input.
41076
+ *
41077
+ * Not through `commit`, and that is the point of the split: a reset is the
41078
+ * form's decision and there is nothing here for a listener to refuse. What it
41079
+ * owes is the report and the platform's own announcement, which is what
41080
+ * `settle` is.
41059
41081
  * - `formDisabledCallback(disabled: boolean) => void`: The platform's notice that an ancestor fieldset changed.
41060
41082
  *
41061
41083
  * **The argument is the effective state, and taking it whole is what latched
package/types/vue.d.ts CHANGED
@@ -1009,7 +1009,7 @@ case is the absent attribute, and the styles read it that way. */
1009
1009
  };
1010
1010
 
1011
1011
  export type HelixHeadlessCarouselVueEvents = {
1012
- /** Fired when the slide in view changes, with its index. Cancelable where the carousel is about to move: refusing a control or an indicator takes the decision and nothing moves. A drag has already moved the track, so that one announces after the fact and refusing it does nothing. See `handleScroll`. */
1012
+ /** Fired before the slide in view changes, with the index being asked for. Cancelable: refusing it takes the decision and nothing moves, including a drag, which is put back on the slide the index still holds. See `handleScroll`. */
1013
1013
  "helix-select": HelixHeadlessCarouselHelixSelectElementEvent;
1014
1014
  };
1015
1015
 
@@ -1102,12 +1102,15 @@ Reflected, because every one of the four is a visual state and a stylesheet
1102
1102
  has no other way to know. Absent is a settled message with nothing to
1103
1103
  report, which is what most of a transcript is, and it is the absence of the
1104
1104
  attribute rather than a value of it: there is no initializer, so nothing is
1105
- reflected until something happens. */
1105
+ reflected until something happens.
1106
+
1107
+ **The turn writes this once, on a retry nobody refused**, and `handleRetry`
1108
+ holds the reason. Everything else that moves it is the consumer's. */
1106
1109
  state?: (HelixHeadlessChatMessage["state"]) | undefined;
1107
1110
  };
1108
1111
 
1109
1112
  export type HelixHeadlessChatMessageVueEvents = {
1110
- /** Fired when the reader asks to send a failed message again, and cancelable. */
1113
+ /** Fired when the reader asks to send a failed message again. Cancelable: a retry that stands puts the turn back on its way, and refusing it leaves the failure and the control offering it exactly where they were. */
1111
1114
  "helix-send": HelixHeadlessChatMessageHelixSendElementEvent;
1112
1115
  };
1113
1116
 
@@ -3211,7 +3214,7 @@ Here rather than on each control. */
3211
3214
  export type HelixHeadlessFileInputVueEvents = {
3212
3215
  /** Announced once the value has moved. */
3213
3216
  change: HelixHeadlessFileInputElementEvent;
3214
- /** Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and nothing moves. */
3217
+ /** Fired before the chosen files change, with their names. Cancelable: refusing it takes the decision and the field keeps the files it had, a pick the browser has already written included. */
3215
3218
  "helix-before-change": HelixHeadlessFileInputHelixBeforeChangeElementEvent;
3216
3219
  /** Announced once the value has moved. */
3217
3220
  input: HelixHeadlessFileInputElementEvent;