@vaadin/field-highlighter 25.3.0-alpha10 → 25.3.0-alpha12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/field-highlighter",
3
- "version": "25.3.0-alpha10",
3
+ "version": "25.3.0-alpha12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,33 +32,33 @@
32
32
  "field"
33
33
  ],
34
34
  "dependencies": {
35
- "@vaadin/a11y-base": "25.3.0-alpha10",
36
- "@vaadin/component-base": "25.3.0-alpha10",
37
- "@vaadin/overlay": "25.3.0-alpha10",
38
- "@vaadin/popover": "25.3.0-alpha10",
39
- "@vaadin/tooltip": "25.3.0-alpha10",
40
- "@vaadin/vaadin-themable-mixin": "25.3.0-alpha10",
35
+ "@vaadin/a11y-base": "25.3.0-alpha12",
36
+ "@vaadin/component-base": "25.3.0-alpha12",
37
+ "@vaadin/overlay": "25.3.0-alpha12",
38
+ "@vaadin/popover": "25.3.0-alpha12",
39
+ "@vaadin/tooltip": "25.3.0-alpha12",
40
+ "@vaadin/vaadin-themable-mixin": "25.3.0-alpha12",
41
41
  "lit": "^3.0.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@vaadin/aura": "25.3.0-alpha10",
45
- "@vaadin/chai-plugins": "25.3.0-alpha10",
46
- "@vaadin/checkbox": "25.3.0-alpha10",
47
- "@vaadin/checkbox-group": "25.3.0-alpha10",
48
- "@vaadin/custom-field": "25.3.0-alpha10",
49
- "@vaadin/date-picker": "25.3.0-alpha10",
50
- "@vaadin/date-time-picker": "25.3.0-alpha10",
51
- "@vaadin/item": "25.3.0-alpha10",
52
- "@vaadin/list-box": "25.3.0-alpha10",
53
- "@vaadin/radio-group": "25.3.0-alpha10",
54
- "@vaadin/select": "25.3.0-alpha10",
55
- "@vaadin/test-runner-commands": "25.3.0-alpha10",
44
+ "@vaadin/aura": "25.3.0-alpha12",
45
+ "@vaadin/chai-plugins": "25.3.0-alpha12",
46
+ "@vaadin/checkbox": "25.3.0-alpha12",
47
+ "@vaadin/checkbox-group": "25.3.0-alpha12",
48
+ "@vaadin/custom-field": "25.3.0-alpha12",
49
+ "@vaadin/date-picker": "25.3.0-alpha12",
50
+ "@vaadin/date-time-picker": "25.3.0-alpha12",
51
+ "@vaadin/item": "25.3.0-alpha12",
52
+ "@vaadin/list-box": "25.3.0-alpha12",
53
+ "@vaadin/radio-group": "25.3.0-alpha12",
54
+ "@vaadin/select": "25.3.0-alpha12",
55
+ "@vaadin/test-runner-commands": "25.3.0-alpha12",
56
56
  "@vaadin/testing-helpers": "^2.0.0",
57
- "@vaadin/text-area": "25.3.0-alpha10",
58
- "@vaadin/text-field": "25.3.0-alpha10",
59
- "@vaadin/time-picker": "25.3.0-alpha10",
60
- "@vaadin/vaadin-lumo-styles": "25.3.0-alpha10",
57
+ "@vaadin/text-area": "25.3.0-alpha12",
58
+ "@vaadin/text-field": "25.3.0-alpha12",
59
+ "@vaadin/time-picker": "25.3.0-alpha12",
60
+ "@vaadin/vaadin-lumo-styles": "25.3.0-alpha12",
61
61
  "sinon": "^22.0.0"
62
62
  },
63
- "gitHead": "f2833abdf9b613fa0d0ed216830e3f4de87b7dac"
63
+ "gitHead": "0f0337783efe38332f5dc6a8968f207d2982de4b"
64
64
  }
@@ -181,6 +181,65 @@ export const aiFieldMarkerStyles = css`
181
181
  }
182
182
  }
183
183
 
184
+ /* The confidence indicator: a sibling of the marker slotted into the
185
+ field's helper text section. The level class name picks the color and how
186
+ much of the pie icon is filled. */
187
+ :has(> vaadin-ai-field-marker) > [slot='helper'].ai-confidence {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: var(--vaadin-gap-s);
191
+ color: var(--_vaadin-ai-field-marker-confidence-color);
192
+
193
+ &::before {
194
+ content: '';
195
+ flex: none;
196
+ box-sizing: border-box;
197
+ width: var(--vaadin-icon-size, 1lh);
198
+ height: var(--vaadin-icon-size, 1lh);
199
+ /* The padding insets the pie from the ring: the wedge is clipped to the
200
+ content box, while the tint fills the whole circle behind it. */
201
+ padding: calc(var(--vaadin-icon-size, 1lh) / 12);
202
+ border: 1px solid color-mix(in srgb, currentColor 50%, transparent);
203
+ border-radius: 50%;
204
+ background-color: color-mix(in srgb, currentColor 15%, transparent);
205
+ background-image: conic-gradient(currentColor var(--_vaadin-ai-field-marker-confidence-fill, 0%), #0000 0%);
206
+ background-clip: content-box, border-box;
207
+ }
208
+
209
+ /* The levels are told apart by how much of the pie is filled, so the base
210
+ styles keep them in the same neutral color. The themes map the custom
211
+ properties to their own semantic colors. */
212
+ &.ai-confidence-low {
213
+ --_vaadin-ai-field-marker-confidence-color: var(
214
+ --vaadin-ai-field-marker-confidence-low-color,
215
+ var(--vaadin-text-color-secondary)
216
+ );
217
+ --_vaadin-ai-field-marker-confidence-fill: 25%;
218
+ }
219
+
220
+ &.ai-confidence-medium {
221
+ --_vaadin-ai-field-marker-confidence-color: var(
222
+ --vaadin-ai-field-marker-confidence-medium-color,
223
+ var(--vaadin-text-color-secondary)
224
+ );
225
+ --_vaadin-ai-field-marker-confidence-fill: 50%;
226
+ }
227
+
228
+ &.ai-confidence-high {
229
+ --_vaadin-ai-field-marker-confidence-color: var(
230
+ --vaadin-ai-field-marker-confidence-high-color,
231
+ var(--vaadin-text-color-secondary)
232
+ );
233
+ --_vaadin-ai-field-marker-confidence-fill: 75%;
234
+ }
235
+ }
236
+
237
+ /* While the AI is working, the confidence describes a value that is about
238
+ to be replaced, so hide it along with the marker. */
239
+ [ai-working] > [slot='helper'].ai-confidence {
240
+ display: none;
241
+ }
242
+
184
243
  [ai-working],
185
244
  :has(> vaadin-ai-field-marker)::part(input-field),
186
245
  :has(> vaadin-ai-field-marker)::part(input-fields),
@@ -28,6 +28,15 @@ export interface AiFieldMarkerI18n {
28
28
  * The tooltip text of the badge button.
29
29
  */
30
30
  badgeTooltip?: string;
31
+
32
+ /**
33
+ * The texts of the confidence indicator.
34
+ */
35
+ confidence?: {
36
+ low?: string;
37
+ medium?: string;
38
+ high?: string;
39
+ };
31
40
  }
32
41
 
33
42
  /**
@@ -53,20 +62,32 @@ export type AiFieldRevertEvent = CustomEvent<{ value: unknown }>;
53
62
  * in progress, set the `working` property to show an "AI is working" shimmer
54
63
  * on the field along with a client-side read-only guard.
55
64
  *
65
+ * Set the `confidence` property to show the confidence level of the filled
66
+ * value (`low`, `medium` or `high`) as an indicator in the field's helper
67
+ * text section, ahead of a helper the field itself may have. While the
68
+ * indicator is shown, the field is marked with `has-helper`, so that the
69
+ * helper text section is laid out the same as for a helper of its own.
70
+ *
56
71
  * ### Styling
57
72
  *
58
- * The following state attribute is set on the field element for styling:
73
+ * The following state attributes are set on the field element for styling:
59
74
  *
60
- * Attribute | Description
61
- * -------------|-------------
62
- * `ai-working` | Set while an AI is working on the field.
75
+ * Attribute | Description
76
+ * ----------------|-------------
77
+ * `ai-working` | Set while an AI is working on the field.
78
+ *
79
+ * The confidence indicator is rendered into the field's light DOM as a
80
+ * `<span>` with the `ai-confidence` class name and the level as an additional
81
+ * `ai-confidence-low`, `ai-confidence-medium` or `ai-confidence-high` one.
63
82
  *
64
83
  * The following custom CSS properties are available for styling:
65
84
  *
66
- * Custom CSS property |
67
- * :-------------------------------------------|
68
- * `--vaadin-ai-field-marker-badge-icon-color` |
69
- * `--vaadin-ai-field-marker-mask-pos` |
85
+ * Custom CSS property |
86
+ * :----------------------------------------------------|
87
+ * `--vaadin-ai-field-marker-badge-icon-color` |
88
+ * `--vaadin-ai-field-marker-confidence-high-color` |
89
+ * `--vaadin-ai-field-marker-confidence-low-color` |
90
+ * `--vaadin-ai-field-marker-confidence-medium-color` |
70
91
  *
71
92
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
72
93
  *
@@ -89,7 +110,13 @@ declare class AiFieldMarker extends I18nMixin<typeof HTMLElement, AiFieldMarkerI
89
110
  * // The accessible label of the badge button and the popover dialog.
90
111
  * badgeLabel: 'AI-provided value',
91
112
  * // The tooltip text of the badge button.
92
- * badgeTooltip: 'Field value modified by AI.\nClick for details'
113
+ * badgeTooltip: 'Field value modified by AI.\nClick for details',
114
+ * // The texts of the confidence indicator.
115
+ * confidence: {
116
+ * low: 'Low confidence',
117
+ * medium: 'Medium confidence',
118
+ * high: 'High confidence'
119
+ * }
93
120
  * }
94
121
  * ```
95
122
  */
@@ -105,6 +132,14 @@ declare class AiFieldMarker extends I18nMixin<typeof HTMLElement, AiFieldMarkerI
105
132
  * the field is marked with `aria-busy`.
106
133
  */
107
134
  working: boolean;
135
+
136
+ /**
137
+ * The confidence level of the AI-filled value, shown as an indicator
138
+ * in the field's helper text section. Possible values are `low`,
139
+ * `medium` and `high`; when not set, no indicator is shown. The
140
+ * indicator texts can be localized with the `i18n` property.
141
+ */
142
+ confidence: 'high' | 'low' | 'medium' | null;
108
143
  }
109
144
 
110
145
  declare global {
@@ -11,7 +11,11 @@ import { getDeepActiveElement, getTabbableElements, isKeyboardActive } from '@va
11
11
  import { registerCSSProperty } from '@vaadin/component-base/src/css-utils.js';
12
12
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
13
13
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
14
- import { addValuesToAttribute, removeValuesFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
14
+ import {
15
+ addValuesToAttribute,
16
+ hasNodeContent,
17
+ removeValuesFromAttribute,
18
+ } from '@vaadin/component-base/src/dom-utils.js';
15
19
  import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
16
20
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
17
21
  import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
@@ -23,10 +27,13 @@ const DEFAULT_I18N = {
23
27
  revert: 'Revert Value',
24
28
  badgeLabel: 'AI-provided value',
25
29
  badgeTooltip: 'Field value modified by AI.\nClick for details',
30
+ confidence: {
31
+ low: 'Low confidence',
32
+ medium: 'Medium confidence',
33
+ high: 'High confidence',
34
+ },
26
35
  };
27
36
 
28
- const POPOVER_TRIGGER = ['click'];
29
-
30
37
  // Half of the 1s working shimmer slide (`--vaadin-ai-field-marker-slide` in
31
38
  // the base styles), so that held-back values land — and the read-only lock
32
39
  // lifts — in the middle of a slide instead of at its edge.
@@ -37,6 +44,14 @@ const MARKER_SLOT = 'ai-field-marker';
37
44
  /** Marks the `<style>` element the marker injects into a field's shadow root. */
38
45
  const MARKER_STYLE_ATTRIBUTE = 'ai-field-marker-styles';
39
46
 
47
+ /**
48
+ * The class name of the confidence indicator the marker adds to the field's
49
+ * light DOM; the level goes on a suffixed class name of its own. Prefixed
50
+ * with `ai-`, since the indicator sits among the application's own children
51
+ * of the field, where a plain `confidence` or `low` would be ambiguous.
52
+ */
53
+ const CONFIDENCE_CLASS = 'ai-confidence';
54
+
40
55
  // The position the shimmer's mask is at, animated by the marker's keyframes.
41
56
  // Registered here rather than with an @property rule in the marker stylesheet,
42
57
  // which is injected into the field's root node: a registration only takes effect
@@ -96,6 +111,9 @@ class DelayedFieldValue {
96
111
  /** The queued value, while `#timer` is pending. */
97
112
  #queuedValue;
98
113
 
114
+ /** Whether the field's own `value` accessor is currently replaced. */
115
+ #installed = false;
116
+
99
117
  constructor(field, delay) {
100
118
  this.#field = field;
101
119
  this.#delay = delay;
@@ -146,12 +164,17 @@ class DelayedFieldValue {
146
164
  this.#timer = setTimeout(() => this.#flush(), this.#delay);
147
165
  },
148
166
  });
167
+ this.#installed = true;
149
168
  }
150
169
 
151
170
  /** Restores the field's own accessor, applying a queued value right away. */
152
171
  uninstall() {
153
- if (Object.getOwnPropertyDescriptor(this.#field, 'value')) {
172
+ // Only remove an own property that this instance defined, so that a field
173
+ // keeping its value in an own property instead of an accessor — which
174
+ // `install()` leaves alone — does not lose it.
175
+ if (this.#installed) {
154
176
  delete this.#field.value;
177
+ this.#installed = false;
155
178
  }
156
179
  this.#flush();
157
180
  }
@@ -204,20 +227,32 @@ class DelayedFieldValue {
204
227
  * directly into the marker's light DOM, so that document-level themes
205
228
  * and user stylesheets can reach them.
206
229
  *
230
+ * Set the `confidence` property to show the confidence level of the filled
231
+ * value (`low`, `medium` or `high`) as an indicator in the field's helper
232
+ * text section, ahead of a helper the field itself may have. While the
233
+ * indicator is shown, the field is marked with `has-helper`, so that the
234
+ * helper text section is laid out the same as for a helper of its own.
235
+ *
207
236
  * ### Styling
208
237
  *
209
- * The following state attribute is set on the field element for styling:
238
+ * The following state attributes are set on the field element for styling:
239
+ *
240
+ * Attribute | Description
241
+ * ----------------|-------------
242
+ * `ai-working` | Set while an AI is working on the field.
210
243
  *
211
- * Attribute | Description
212
- * -------------|-------------
213
- * `ai-working` | Set while an AI is working on the field.
244
+ * The confidence indicator is rendered into the field's light DOM as a
245
+ * `<span>` with the `ai-confidence` class name and the level as an additional
246
+ * `ai-confidence-low`, `ai-confidence-medium` or `ai-confidence-high` one.
214
247
  *
215
248
  * The following custom CSS properties are available for styling:
216
249
  *
217
- * Custom CSS property |
218
- * :-------------------------------------------|
219
- * `--vaadin-ai-field-marker-badge-icon-color` |
220
- * `--vaadin-ai-field-marker-mask-pos` |
250
+ * Custom CSS property |
251
+ * :----------------------------------------------------|
252
+ * `--vaadin-ai-field-marker-badge-icon-color` |
253
+ * `--vaadin-ai-field-marker-confidence-high-color` |
254
+ * `--vaadin-ai-field-marker-confidence-low-color` |
255
+ * `--vaadin-ai-field-marker-confidence-medium-color` |
221
256
  *
222
257
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
223
258
  *
@@ -251,6 +286,17 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
251
286
  type: Boolean,
252
287
  value: false,
253
288
  },
289
+
290
+ /**
291
+ * The confidence level of the AI-filled value, shown as an indicator
292
+ * in the field's helper text section. Possible values are `low`,
293
+ * `medium` and `high`; when not set, no indicator is shown. The
294
+ * indicator texts can be localized with the `i18n` property.
295
+ */
296
+ confidence: {
297
+ type: String,
298
+ value: null,
299
+ },
254
300
  };
255
301
  }
256
302
 
@@ -272,6 +318,20 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
272
318
  /** The field value captured for the revert event detail. */
273
319
  #capturedValue;
274
320
 
321
+ /**
322
+ * The confidence indicator added to the field's light DOM and rendered
323
+ * in the field's helper text section. Set while `confidence` is set on
324
+ * a marked field.
325
+ */
326
+ #confidenceNode = null;
327
+
328
+ /**
329
+ * Observes the field's `has-helper` attribute while the confidence
330
+ * indicator is shown, so that the marker can re-assert it if the field
331
+ * recomputes it from its own helper content. Created on first use.
332
+ */
333
+ #helperStateObserver = null;
334
+
275
335
  /**
276
336
  * While in the working state, the elements whose client-side `readonly`
277
337
  * state was overridden — the field itself and, for a `vaadin-custom-field`,
@@ -361,7 +421,13 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
361
421
  * // The accessible label of the badge button and the popover dialog.
362
422
  * badgeLabel: 'AI-provided value',
363
423
  * // The tooltip text of the badge button.
364
- * badgeTooltip: 'Field value modified by AI.\nClick for details'
424
+ * badgeTooltip: 'Field value modified by AI.\nClick for details',
425
+ * // The texts of the confidence indicator.
426
+ * confidence: {
427
+ * low: 'Low confidence',
428
+ * medium: 'Medium confidence',
429
+ * high: 'High confidence'
430
+ * }
365
431
  * }
366
432
  * ```
367
433
  *
@@ -443,6 +509,8 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
443
509
 
444
510
  this.#stopWorking(true);
445
511
 
512
+ this.#removeConfidenceNode();
513
+
446
514
  if (this.#descNode) {
447
515
  removeValuesFromAttribute(this.#describedElement, 'aria-describedby', this.#descNode.id);
448
516
  this.#descNode.remove();
@@ -477,6 +545,10 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
477
545
  return;
478
546
  }
479
547
 
548
+ if (props.has('confidence') || props.has('__effectiveI18n')) {
549
+ this.#updateConfidence();
550
+ }
551
+
480
552
  if (props.has('working')) {
481
553
  if (this.working) {
482
554
  this.#startWorking();
@@ -487,6 +559,12 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
487
559
  this.#capturedValue = this.#annotatedValue();
488
560
  this.#announcePending = true;
489
561
  }
562
+
563
+ // The indicator is hidden while working, so the helper text section is
564
+ // only claimed for it — and the indicator described — once the working
565
+ // state ends.
566
+ this.#updateConfidenceDescription();
567
+ this.#updateFieldHelperState();
490
568
  }
491
569
 
492
570
  // Announce after the update so the announcement reflects a message set in
@@ -510,15 +588,7 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
510
588
  return html`
511
589
  <button id="${this.#badgeId}" class="badge" type="button" tabindex="0" aria-label="${badgeLabel}"></button>
512
590
  <vaadin-tooltip for="${this.#badgeId}" text="${badgeTooltip}"></vaadin-tooltip>
513
- <vaadin-popover
514
- for="${this.#badgeId}"
515
- role="dialog"
516
- aria-label="${badgeLabel}"
517
- .trigger="${POPOVER_TRIGGER}"
518
- autofocus
519
- theme="arrow"
520
- position="end-top"
521
- >
591
+ <vaadin-popover for="${this.#badgeId}" aria-label="${badgeLabel}" autofocus theme="arrow" position="end-top">
522
592
  <p class="message">${message}</p>
523
593
  <div class="actions">
524
594
  <button type="button" tabindex="0" @click="${this.#onRevert}">${revert}</button>
@@ -596,6 +666,10 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
596
666
  this.#describedElement = describedElement;
597
667
  }
598
668
 
669
+ // Apply the confidence indicator directly: on a reconnect no property
670
+ // change triggers updated(), which handles the first connect.
671
+ this.#updateConfidence();
672
+
599
673
  // Capture the AI-filled value so the revert event can carry it.
600
674
  this.#capturedValue = this.#annotatedValue();
601
675
 
@@ -621,6 +695,133 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
621
695
  return this.#valueDelay ? this.#valueDelay.latestValue : field.value;
622
696
  }
623
697
 
698
+ /**
699
+ * Syncs the confidence indicator in the field's helper text section with
700
+ * the `confidence` property: a `<span>` slotted into the field's helper
701
+ * slot, with the `ai-confidence` and `ai-confidence-<level>` class names
702
+ * and the localized level text as content.
703
+ */
704
+ #updateConfidence() {
705
+ const field = this.#field;
706
+ if (!field) {
707
+ return;
708
+ }
709
+
710
+ const level = this.confidence;
711
+ if (!level) {
712
+ this.#removeConfidenceNode();
713
+ return;
714
+ }
715
+
716
+ if (!this.#confidenceNode) {
717
+ const node = document.createElement('span');
718
+ node.setAttribute('slot', 'helper');
719
+ // Hide the indicator from the field's helper slot controller, which
720
+ // would otherwise evict the field's own helper element in favor of
721
+ // the indicator. The browser still renders it in the helper slot.
722
+ node.setAttribute('data-slot-ignore', '');
723
+ node.id = `ai-field-marker-confidence-${generateUniqueId()}`;
724
+ // Insert ahead of a helper the field already has, so that the indicator
725
+ // comes first in the helper text section. A helper added later ends up
726
+ // after the indicator, since the field appends it.
727
+ field.insertBefore(node, field.querySelector(':scope > [slot="helper"]'));
728
+ this.#confidenceNode = node;
729
+ }
730
+
731
+ this.#confidenceNode.className = `${CONFIDENCE_CLASS} ${CONFIDENCE_CLASS}-${level}`;
732
+ this.#confidenceNode.textContent = this.__effectiveI18n.confidence[level] ?? '';
733
+ this.#updateConfidenceDescription();
734
+ this.#updateFieldHelperState();
735
+ }
736
+
737
+ /**
738
+ * Keeps the indicator's id in the described element's `aria-describedby`
739
+ * only while the indicator is shown: a visually hidden indicator would
740
+ * still get read as part of the field's description, although it describes
741
+ * a value the AI is about to replace.
742
+ */
743
+ #updateConfidenceDescription() {
744
+ const node = this.#confidenceNode;
745
+ if (!node || !this.#describedElement) {
746
+ return;
747
+ }
748
+
749
+ if (this.working) {
750
+ removeValuesFromAttribute(this.#describedElement, 'aria-describedby', node.id);
751
+ } else {
752
+ addValuesToAttribute(this.#describedElement, 'aria-describedby', node.id);
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Keeps the field's `has-helper` attribute set while the indicator is
758
+ * shown, since it is content in the field's helper text section although
759
+ * the field's own helper is not what provides it. The attribute is what
760
+ * both the field and the themes key their helper text section styles on —
761
+ * from showing the section at all to placing it above the field for the
762
+ * `helper-above-field` theme.
763
+ *
764
+ * The field recomputes the attribute from its own helper content, which
765
+ * never includes the indicator, so a recomputation can drop it while the
766
+ * indicator is still shown. An observer re-asserts it in that case.
767
+ */
768
+ #updateFieldHelperState() {
769
+ const field = this.#field;
770
+
771
+ // While the AI is working the indicator is hidden, so the field should
772
+ // only reserve the helper text section for a helper of its own.
773
+ if (this.#confidenceNode && !this.working) {
774
+ field.toggleAttribute('has-helper', true);
775
+
776
+ this.#helperStateObserver ??= new MutationObserver(() => {
777
+ // Read the field live: the observer is reused when the marker moves
778
+ // to another field, so a captured one could be a previous field.
779
+ const observedField = this.#field;
780
+ if (observedField && this.#confidenceNode && !this.working && !observedField.hasAttribute('has-helper')) {
781
+ observedField.toggleAttribute('has-helper', true);
782
+ }
783
+ });
784
+ this.#helperStateObserver.observe(field, { attributes: true, attributeFilter: ['has-helper'] });
785
+ return;
786
+ }
787
+
788
+ this.#helperStateObserver?.disconnect();
789
+
790
+ // The field keeps the attribute when its own helper provides content,
791
+ // which it may have gained while the indicator was shown.
792
+ if (!this.#hasFieldHelper()) {
793
+ field.removeAttribute('has-helper');
794
+ }
795
+ }
796
+
797
+ /**
798
+ * Whether the field has helper content of its own, i.e. helper slot content
799
+ * other than the indicator. Judged with the same content check the field
800
+ * itself uses for its `has-helper` attribute.
801
+ *
802
+ * @return {boolean}
803
+ */
804
+ #hasFieldHelper() {
805
+ return [...this.#field.querySelectorAll(':scope > [slot="helper"]')].some(
806
+ (node) => node !== this.#confidenceNode && hasNodeContent(node),
807
+ );
808
+ }
809
+
810
+ /** Removes the confidence indicator. */
811
+ #removeConfidenceNode() {
812
+ const node = this.#confidenceNode;
813
+ if (!node) {
814
+ return;
815
+ }
816
+
817
+ if (this.#describedElement) {
818
+ removeValuesFromAttribute(this.#describedElement, 'aria-describedby', node.id);
819
+ }
820
+ node.remove();
821
+ this.#confidenceNode = null;
822
+ this.#updateFieldHelperState();
823
+ }
824
+
624
825
  /**
625
826
  * Enters the "AI is working" state: shows the shimmer and makes the field
626
827
  * read-only on the client so the user cannot edit a value the AI is about
@@ -673,9 +874,6 @@ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitE
673
874
  */
674
875
  #stopWorking(immediate = false) {
675
876
  const field = this.#field;
676
- if (!field) {
677
- return;
678
- }
679
877
 
680
878
  if (this.#restoreTimer != null) {
681
879
  // Already winding down. Finish it now when the marker is going away, so