@vaadin/field-highlighter 25.2.7 → 25.3.0-alpha10

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.2.7",
3
+ "version": "25.3.0-alpha10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,30 +32,33 @@
32
32
  "field"
33
33
  ],
34
34
  "dependencies": {
35
- "@vaadin/a11y-base": "~25.2.7",
36
- "@vaadin/component-base": "~25.2.7",
37
- "@vaadin/overlay": "~25.2.7",
38
- "@vaadin/vaadin-themable-mixin": "~25.2.7",
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",
39
41
  "lit": "^3.0.0"
40
42
  },
41
43
  "devDependencies": {
42
- "@vaadin/aura": "~25.2.7",
43
- "@vaadin/chai-plugins": "~25.2.7",
44
- "@vaadin/checkbox": "~25.2.7",
45
- "@vaadin/checkbox-group": "~25.2.7",
46
- "@vaadin/date-picker": "~25.2.7",
47
- "@vaadin/date-time-picker": "~25.2.7",
48
- "@vaadin/item": "~25.2.7",
49
- "@vaadin/list-box": "~25.2.7",
50
- "@vaadin/radio-group": "~25.2.7",
51
- "@vaadin/select": "~25.2.7",
52
- "@vaadin/test-runner-commands": "~25.2.7",
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",
53
56
  "@vaadin/testing-helpers": "^2.0.0",
54
- "@vaadin/text-area": "~25.2.7",
55
- "@vaadin/text-field": "~25.2.7",
56
- "@vaadin/time-picker": "~25.2.7",
57
- "@vaadin/vaadin-lumo-styles": "~25.2.7",
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",
58
61
  "sinon": "^22.0.0"
59
62
  },
60
- "gitHead": "14be1af674f3968cce68c6eb4742340694c4a949"
63
+ "gitHead": "f2833abdf9b613fa0d0ed216830e3f4de87b7dac"
61
64
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { CSSResult } from 'lit';
7
+
8
+ export const aiFieldMarkerStyles: CSSResult;
9
+
10
+ export const aiFieldMarkerHostStyles: CSSResult;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { css } from 'lit';
7
+ import { screenReaderOnly } from '@vaadin/a11y-base/src/styles/sr-only-styles.js';
8
+
9
+ export const aiFieldMarkerHostStyles = css`
10
+ @keyframes --vaadin-ai-field-marker-slide {
11
+ 0% {
12
+ --vaadin-ai-field-marker-mask-pos: -100px;
13
+ }
14
+
15
+ 100% {
16
+ --vaadin-ai-field-marker-mask-pos: calc(100% + 100px);
17
+ }
18
+ }
19
+
20
+ @keyframes --vaadin-ai-field-marker-remove-mask {
21
+ 100% {
22
+ mask-image: none;
23
+ }
24
+ }
25
+ `;
26
+
27
+ /**
28
+ * Styles applied to the `<vaadin-ai-field-marker>` element itself: the badge
29
+ * button anchored to the field's top corner and the popover content.
30
+ */
31
+ export const aiFieldMarkerStyles = css`
32
+ :has(> vaadin-ai-field-marker) {
33
+ position: relative;
34
+ }
35
+
36
+ /* --vaadin-ai-field-marker-mask-pos is registered from JS: an @property rule only
37
+ takes effect at document scope, and this sheet is injected into the field's
38
+ root node, which is a shadow root for a nested field. */
39
+
40
+ vaadin-ai-field-marker[hidden] {
41
+ display: none !important;
42
+ }
43
+
44
+ :where(vaadin-ai-field-marker) {
45
+ display: contents;
46
+ --_vaadin-ai-field-marker-color-1: light-dark(#932fffd9, #bc64ff);
47
+ --_vaadin-ai-field-marker-color-2: light-dark(#004cffcc, #539aff);
48
+
49
+ &::before {
50
+ content: '';
51
+ position: absolute;
52
+ inset: -6px;
53
+ border-radius: 9px;
54
+ z-index: -1;
55
+ pointer-events: none;
56
+ background-color: color-mix(in srgb, var(--_vaadin-ai-field-marker-color-2) 30%, transparent);
57
+ background-image:
58
+ radial-gradient(66.92% 123.25% at 100% 88.78%, var(--_vaadin-ai-field-marker-color-1) 0%, transparent 100%),
59
+ radial-gradient(42.57% 69.91% at 14.85% 33.33%, var(--_vaadin-ai-field-marker-color-2) 0%, transparent 100%);
60
+ opacity: 0.16;
61
+ mask-image: linear-gradient(
62
+ 90deg,
63
+ #000,
64
+ #000 var(--vaadin-ai-field-marker-mask-pos),
65
+ transparent calc(var(--vaadin-ai-field-marker-mask-pos) + 20px),
66
+ transparent
67
+ );
68
+ animation: --vaadin-ai-field-marker-slide 700ms 200ms both;
69
+ animation-timing-function: cubic-bezier(0.78, 0, 0.22, 1);
70
+ }
71
+
72
+ .badge {
73
+ all: initial;
74
+ position: absolute;
75
+ top: -6px;
76
+ inset-inline-end: -6px;
77
+ box-sizing: border-box;
78
+ display: inline-flex;
79
+ align-items: center;
80
+ padding: 4px;
81
+ border: none;
82
+ border-radius: var(--vaadin-radius-m);
83
+ margin: 0;
84
+ background: transparent;
85
+ color: var(--vaadin-ai-field-marker-badge-icon-color, var(--vaadin-text-color-secondary));
86
+ font: inherit;
87
+ font-size: 1rem;
88
+ line-height: 1;
89
+ cursor: pointer;
90
+ transition: color 200ms;
91
+ animation: --vaadin-ai-field-marker-fade-in 300ms 700ms backwards;
92
+
93
+ &:hover {
94
+ color: var(--vaadin-ai-field-marker-badge-icon-color, var(--vaadin-text-color));
95
+ }
96
+
97
+ &::before {
98
+ content: '';
99
+ display: block;
100
+ width: 1lh;
101
+ height: 1lh;
102
+ background: currentColor;
103
+ --_icon-ai-badge: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="M7.18848 8.48926H5.46289L6.32715 5.8623L7.18848 8.48926Z" fill="black"/><path fill-rule="evenodd" clip-rule="evenodd" d="M11 0C13.7614 0 16 2.23858 16 5V11C16 13.7614 13.7614 16 11 16H5C2.23858 16 0 13.7614 0 11V5C0 2.23858 2.23858 0 5 0H11ZM5.58203 4.52051L3.25977 11H4.63672L5.10742 9.56934H7.54297L8.01172 11H9.45215L7.12988 4.52051H5.58203ZM10.1211 4.52051V11H11.5068V4.52051H10.1211Z" fill="black"/></svg>');
104
+ mask-image: var(--_icon-ai-badge);
105
+ }
106
+
107
+ &::after {
108
+ content: '';
109
+ position: absolute;
110
+ width: 24px;
111
+ height: 24px;
112
+ top: 50%;
113
+ left: 50%;
114
+ translate: -50% -50%;
115
+ }
116
+ }
117
+
118
+ .badge:focus-visible {
119
+ outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
120
+ }
121
+
122
+ > vaadin-popover::part(content) {
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: var(--vaadin-gap-s);
126
+ padding: var(--vaadin-padding-m);
127
+ max-width: 20em;
128
+ }
129
+
130
+ .message {
131
+ margin: 0;
132
+ }
133
+
134
+ /* The hidden AI-fill description linked to the field's input via
135
+ aria-describedby: visually hidden but kept in the accessibility tree,
136
+ using the sr-only styles from @vaadin/a11y-base. */
137
+ ${screenReaderOnly}
138
+
139
+ .actions {
140
+ display: flex;
141
+ gap: var(--vaadin-gap-xs);
142
+ }
143
+
144
+ .actions > button {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: var(--vaadin-gap-s);
148
+ pointer-events: auto;
149
+ box-sizing: border-box;
150
+ padding: var(--vaadin-padding-block-container) var(--vaadin-padding-inline-container);
151
+ margin: calc(var(--vaadin-padding-block-container) * -1) calc(var(--vaadin-padding-inline-container) * -1);
152
+ border: 0;
153
+ border-radius: var(--vaadin-radius-m);
154
+ background: transparent;
155
+ color: var(--vaadin-text-color);
156
+ font: inherit;
157
+ font-weight: 500;
158
+ cursor: pointer;
159
+ transition: background-color 100ms;
160
+
161
+ &:hover {
162
+ background: var(--vaadin-background-container);
163
+ }
164
+
165
+ &:active {
166
+ background: var(--vaadin-background-container-strong);
167
+ }
168
+
169
+ &:focus-visible {
170
+ outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
171
+ }
172
+
173
+ &::before {
174
+ content: '';
175
+ display: inline-block;
176
+ width: 1em;
177
+ height: 1em;
178
+ background: currentColor;
179
+ mask: var(--_vaadin-icon-undo);
180
+ }
181
+ }
182
+ }
183
+
184
+ [ai-working],
185
+ :has(> vaadin-ai-field-marker)::part(input-field),
186
+ :has(> vaadin-ai-field-marker)::part(input-fields),
187
+ :has(> vaadin-ai-field-marker)::part(group-field) {
188
+ mask-image: linear-gradient(
189
+ 90deg,
190
+ #000 calc(var(--vaadin-ai-field-marker-mask-pos) - 100px),
191
+ rgba(0, 0, 0, 0.3) calc(var(--vaadin-ai-field-marker-mask-pos) - 70px),
192
+ rgba(0, 0, 0, 0.3) var(--vaadin-ai-field-marker-mask-pos),
193
+ #000 calc(var(--vaadin-ai-field-marker-mask-pos) + 70px),
194
+ #000 calc(var(--vaadin-ai-field-marker-mask-pos) + 100px)
195
+ );
196
+ animation:
197
+ --vaadin-ai-field-marker-slide 1s cubic-bezier(0.78, 0, 0.22, 1) forwards,
198
+ --vaadin-ai-field-marker-remove-mask 0s 1s forwards;
199
+ }
200
+
201
+ [ai-working] {
202
+ animation: --vaadin-ai-field-marker-slide 1s ease-in-out infinite;
203
+ }
204
+
205
+ /* While the AI is working, the badge and glow describe a value that is about
206
+ to be replaced, so hide the marker until the field leaves the working
207
+ state. Hiding rather than unmarking keeps the previous mark for a fill that
208
+ is cancelled or fails. */
209
+ [ai-working] > vaadin-ai-field-marker {
210
+ display: none;
211
+ }
212
+
213
+ @keyframes --vaadin-ai-field-marker-fade-in {
214
+ 0% {
215
+ opacity: 0;
216
+ }
217
+ }
218
+
219
+ ${aiFieldMarkerHostStyles}
220
+ `;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
7
+
8
+ /**
9
+ * Localization texts for {@link AiFieldMarker}.
10
+ */
11
+ export interface AiFieldMarkerI18n {
12
+ /**
13
+ * The message shown in the popover explaining the AI fill.
14
+ */
15
+ message?: string;
16
+
17
+ /**
18
+ * The label of the revert control.
19
+ */
20
+ revert?: string;
21
+
22
+ /**
23
+ * The accessible label of the badge button and the popover dialog.
24
+ */
25
+ badgeLabel?: string;
26
+
27
+ /**
28
+ * The tooltip text of the badge button.
29
+ */
30
+ badgeTooltip?: string;
31
+ }
32
+
33
+ /**
34
+ * Fired from the field element when the user activates the revert control.
35
+ * The host is expected to restore the field's previous value.
36
+ *
37
+ * The event bubbles from the field rather than from the marker, so it is not
38
+ * declared on a listener map: listen for it on the field or a container, and
39
+ * annotate the listener with this type.
40
+ */
41
+ export type AiFieldRevertEvent = CustomEvent<{ value: unknown }>;
42
+
43
+ /**
44
+ * An element used internally by Vaadin. Not intended to be used separately.
45
+ *
46
+ * Annotates a field as AI-filled: appended as a direct child of the field,
47
+ * it slots itself into the field via a slot injected into the field's shadow
48
+ * root, draws an "AI" badge anchored to the field, and offers a popover that
49
+ * explains the AI fill and lets the user revert the value.
50
+ *
51
+ * The marker manages the annotation through its own lifecycle: adding it to
52
+ * the field marks the field, removing it clears the mark. While an AI fill is
53
+ * in progress, set the `working` property to show an "AI is working" shimmer
54
+ * on the field along with a client-side read-only guard.
55
+ *
56
+ * ### Styling
57
+ *
58
+ * The following state attribute is set on the field element for styling:
59
+ *
60
+ * Attribute | Description
61
+ * -------------|-------------
62
+ * `ai-working` | Set while an AI is working on the field.
63
+ *
64
+ * The following custom CSS properties are available for styling:
65
+ *
66
+ * Custom CSS property |
67
+ * :-------------------------------------------|
68
+ * `--vaadin-ai-field-marker-badge-icon-color` |
69
+ * `--vaadin-ai-field-marker-mask-pos` |
70
+ *
71
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
72
+ *
73
+ * @fires {CustomEvent} ai-field-revert - Fired from the field element when the user activates the revert control. The host restores the value.
74
+ */
75
+ declare class AiFieldMarker extends I18nMixin<typeof HTMLElement, AiFieldMarkerI18n>(HTMLElement) {
76
+ /**
77
+ * The object used to localize this component. To change the default
78
+ * localization, replace this with an object that provides all properties, or
79
+ * just the individual properties you want to change.
80
+ *
81
+ * The object has the following JSON structure and default values:
82
+ *
83
+ * ```
84
+ * {
85
+ * // The message shown in the popover explaining the AI fill.
86
+ * message: 'This field value was modified by AI.',
87
+ * // The label of the revert control.
88
+ * revert: 'Revert Value',
89
+ * // The accessible label of the badge button and the popover dialog.
90
+ * badgeLabel: 'AI-provided value',
91
+ * // The tooltip text of the badge button.
92
+ * badgeTooltip: 'Field value modified by AI.\nClick for details'
93
+ * }
94
+ * ```
95
+ */
96
+ i18n: AiFieldMarkerI18n;
97
+
98
+ /**
99
+ * Whether an AI is currently working on the field. While `true`, the field
100
+ * shows an "AI is working" shimmer and is made read-only on the client so
101
+ * the user cannot edit a value the AI is about to overwrite; only the
102
+ * client-side `readonly` state is touched, and setting the property back to
103
+ * `false` restores it. The marker badge is hidden for the duration, since
104
+ * the value it annotates is about to be replaced. For assistive technology,
105
+ * the field is marked with `aria-busy`.
106
+ */
107
+ working: boolean;
108
+ }
109
+
110
+ declare global {
111
+ interface HTMLElementTagNameMap {
112
+ 'vaadin-ai-field-marker': AiFieldMarker;
113
+ }
114
+ }
115
+
116
+ export { AiFieldMarker };
@@ -0,0 +1,773 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import '@vaadin/popover/src/vaadin-popover.js';
7
+ import '@vaadin/tooltip/src/vaadin-tooltip.js';
8
+ import { html, LitElement, nothing } from 'lit';
9
+ import { announce } from '@vaadin/a11y-base/src/announce.js';
10
+ import { getDeepActiveElement, getTabbableElements, isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
11
+ import { registerCSSProperty } from '@vaadin/component-base/src/css-utils.js';
12
+ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
13
+ import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
14
+ import { addValuesToAttribute, removeValuesFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
15
+ import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
16
+ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
17
+ import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
18
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
19
+ import { aiFieldMarkerHostStyles, aiFieldMarkerStyles } from './styles/vaadin-ai-field-marker-base-styles.js';
20
+
21
+ const DEFAULT_I18N = {
22
+ message: 'This field value was modified by AI.',
23
+ revert: 'Revert Value',
24
+ badgeLabel: 'AI-provided value',
25
+ badgeTooltip: 'Field value modified by AI.\nClick for details',
26
+ };
27
+
28
+ const POPOVER_TRIGGER = ['click'];
29
+
30
+ // Half of the 1s working shimmer slide (`--vaadin-ai-field-marker-slide` in
31
+ // the base styles), so that held-back values land — and the read-only lock
32
+ // lifts — in the middle of a slide instead of at its edge.
33
+ const HALF_WORKING_SLIDE_MS = 500;
34
+
35
+ const MARKER_SLOT = 'ai-field-marker';
36
+
37
+ /** Marks the `<style>` element the marker injects into a field's shadow root. */
38
+ const MARKER_STYLE_ATTRIBUTE = 'ai-field-marker-styles';
39
+
40
+ // The position the shimmer's mask is at, animated by the marker's keyframes.
41
+ // Registered here rather than with an @property rule in the marker stylesheet,
42
+ // which is injected into the field's root node: a registration only takes effect
43
+ // at document scope, and that root node is a shadow root for a field nested
44
+ // inside another component.
45
+ registerCSSProperty({
46
+ name: '--vaadin-ai-field-marker-mask-pos',
47
+ syntax: '<length-percentage>',
48
+ inherits: false,
49
+ initialValue: '0px',
50
+ });
51
+
52
+ /**
53
+ * Adds the marker's keyframes to the field's own shadow root, where the
54
+ * animation names used by the `::part()` rules above have to resolve, since
55
+ * keyframes are looked up in the tree scope of the animated element.
56
+ *
57
+ * Injected as a `<style>` element rather than an adopted stylesheet because the
58
+ * themable infrastructure replaces `adoptedStyleSheets` wholesale — on a Lumo
59
+ * stylesheet load or a theme switch — which would silently drop the keyframes
60
+ * and leave the field's input masked but never animating.
61
+ *
62
+ * @param {HTMLElement} field
63
+ */
64
+ function injectMarkerHostStyles(field) {
65
+ if (field.shadowRoot.querySelector(`style[${MARKER_STYLE_ATTRIBUTE}]`)) {
66
+ return;
67
+ }
68
+
69
+ const style = document.createElement('style');
70
+ style.setAttribute(MARKER_STYLE_ATTRIBUTE, '');
71
+ style.textContent = aiFieldMarkerHostStyles.cssText;
72
+ field.shadowRoot.appendChild(style);
73
+ }
74
+
75
+ /**
76
+ * Holds back value assignments on a field, so that the value an AI fills in
77
+ * lands halfway through the marker's slide animation instead of instantly.
78
+ *
79
+ * While installed, the field's own `value` accessor is replaced with one that
80
+ * queues an assignment and applies it after the delay; a further assignment
81
+ * supersedes a queued one. Uninstalling restores the field's own accessor and
82
+ * applies a queued assignment right away, since it carries the value the
83
+ * marker was working on and nothing may land after the accessor is restored —
84
+ * a late-landing value would overwrite one the host has set since.
85
+ */
86
+ class DelayedFieldValue {
87
+ /** The intercepted accessor, found on the field's prototype chain. */
88
+ #descriptor;
89
+
90
+ #field;
91
+
92
+ #delay;
93
+
94
+ #timer = null;
95
+
96
+ /** The queued value, while `#timer` is pending. */
97
+ #queuedValue;
98
+
99
+ constructor(field, delay) {
100
+ this.#field = field;
101
+ this.#delay = delay;
102
+
103
+ let descriptor = null;
104
+ for (let proto = Object.getPrototypeOf(field); proto && !descriptor; proto = Object.getPrototypeOf(proto)) {
105
+ descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
106
+ }
107
+ this.#descriptor = descriptor?.get && descriptor.set ? descriptor : null;
108
+ }
109
+
110
+ /**
111
+ * Whether the field exposes a `value` accessor that can be intercepted.
112
+ * Without one there is nothing to delegate to, and defining an own `value`
113
+ * would make `'value' in field` report a value the field does not have.
114
+ *
115
+ * @return {boolean}
116
+ */
117
+ get supported() {
118
+ return this.#descriptor != null;
119
+ }
120
+
121
+ /**
122
+ * The value the field ends up with: a queued value while one is pending,
123
+ * otherwise the field's current value. Reading `field.value` instead would
124
+ * return the value that the queued assignment is about to replace.
125
+ *
126
+ * @return {unknown}
127
+ */
128
+ get latestValue() {
129
+ return this.#timer != null ? this.#queuedValue : this.#field.value;
130
+ }
131
+
132
+ /** Starts holding back value assignments. Keeps a queued assignment. */
133
+ install() {
134
+ const field = this.#field;
135
+ if (!this.supported || Object.getOwnPropertyDescriptor(field, 'value')) {
136
+ return;
137
+ }
138
+
139
+ const descriptor = this.#descriptor;
140
+ Object.defineProperty(field, 'value', {
141
+ configurable: true,
142
+ get: () => descriptor.get.call(field),
143
+ set: (value) => {
144
+ this.#queuedValue = value;
145
+ clearTimeout(this.#timer);
146
+ this.#timer = setTimeout(() => this.#flush(), this.#delay);
147
+ },
148
+ });
149
+ }
150
+
151
+ /** Restores the field's own accessor, applying a queued value right away. */
152
+ uninstall() {
153
+ if (Object.getOwnPropertyDescriptor(this.#field, 'value')) {
154
+ delete this.#field.value;
155
+ }
156
+ this.#flush();
157
+ }
158
+
159
+ /**
160
+ * Applies a queued value right away. Applied through the intercepted
161
+ * accessor, so an installed hold-back stays in place for further
162
+ * assignments — this is also how a queued value lands on its deadline.
163
+ */
164
+ #flush() {
165
+ if (this.#timer == null) {
166
+ return;
167
+ }
168
+
169
+ clearTimeout(this.#timer);
170
+ this.#timer = null;
171
+ const value = this.#queuedValue;
172
+ this.#queuedValue = null;
173
+ this.#descriptor.set.call(this.#field, value);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * An element used internally by Vaadin. Not intended to be used separately.
179
+ *
180
+ * Annotates a field as AI-filled: appended as a direct child of the field,
181
+ * it slots itself into the field via a slot injected into the field's shadow
182
+ * root, draws an "AI" badge anchored to the field, and offers a popover that
183
+ * explains the AI fill and lets the user revert the value.
184
+ *
185
+ * The marker manages the annotation through its own lifecycle: adding it to
186
+ * the field marks the field, removing it clears the mark:
187
+ *
188
+ * ```js
189
+ * const marker = document.createElement('vaadin-ai-field-marker');
190
+ * marker.i18n = { message: 'Filled based on the uploaded document.' };
191
+ * field.appendChild(marker);
192
+ * // ...
193
+ * marker.remove();
194
+ * ```
195
+ *
196
+ * While an AI fill is in progress, set the `working` property to show an
197
+ * "AI is working" shimmer on the field along with a client-side read-only
198
+ * guard. An existing mark is hidden for the duration, since the value it
199
+ * annotates is about to be replaced; setting `working` back to `false`
200
+ * brings it back, so a cancelled or failed fill leaves the mark intact.
201
+ *
202
+ * The pieces that construct the marker — the badge, its tooltip and the
203
+ * popover with the explanation and the revert control — are rendered
204
+ * directly into the marker's light DOM, so that document-level themes
205
+ * and user stylesheets can reach them.
206
+ *
207
+ * ### Styling
208
+ *
209
+ * The following state attribute is set on the field element for styling:
210
+ *
211
+ * Attribute | Description
212
+ * -------------|-------------
213
+ * `ai-working` | Set while an AI is working on the field.
214
+ *
215
+ * The following custom CSS properties are available for styling:
216
+ *
217
+ * Custom CSS property |
218
+ * :-------------------------------------------|
219
+ * `--vaadin-ai-field-marker-badge-icon-color` |
220
+ * `--vaadin-ai-field-marker-mask-pos` |
221
+ *
222
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
223
+ *
224
+ * @fires {CustomEvent} ai-field-revert - Fired from the field element when the user activates the revert control. The host restores the value.
225
+ *
226
+ * @customElement vaadin-ai-field-marker
227
+ * @extends HTMLElement
228
+ * @private
229
+ */
230
+ class AiFieldMarker extends SlotStylesMixin(I18nMixin(DirMixin(PolylitMixin(LitElement)))) {
231
+ static get is() {
232
+ return 'vaadin-ai-field-marker';
233
+ }
234
+
235
+ static get defaultI18n() {
236
+ return DEFAULT_I18N;
237
+ }
238
+
239
+ static get properties() {
240
+ return {
241
+ /**
242
+ * Whether an AI is currently working on the field. While `true`, the
243
+ * field shows an "AI is working" shimmer and is made read-only on the
244
+ * client so the user cannot edit a value the AI is about to overwrite;
245
+ * only the client-side `readonly` state is touched, and setting the
246
+ * property back to `false` restores it. The marker badge is hidden for
247
+ * the duration, since the value it annotates is about to be replaced.
248
+ * For assistive technology, the field is marked with `aria-busy`.
249
+ */
250
+ working: {
251
+ type: Boolean,
252
+ value: false,
253
+ },
254
+ };
255
+ }
256
+
257
+ /**
258
+ * The field the marker annotates: its parent element. Set while the marker
259
+ * is connected to a field with a shadow root.
260
+ */
261
+ #field = null;
262
+
263
+ /**
264
+ * The hidden description node added to the marker's light DOM and linked
265
+ * to the field's input via `aria-describedby`.
266
+ */
267
+ #descNode = null;
268
+
269
+ /** The element whose `aria-describedby` references the description node. */
270
+ #describedElement = null;
271
+
272
+ /** The field value captured for the revert event detail. */
273
+ #capturedValue;
274
+
275
+ /**
276
+ * While in the working state, the elements whose client-side `readonly`
277
+ * state was overridden — the field itself and, for a `vaadin-custom-field`,
278
+ * its inputs — with their original values, so leaving the working state can
279
+ * restore them. `null` once the state has been restored.
280
+ */
281
+ #lockedElements = null;
282
+
283
+ /**
284
+ * The pending restore of the captured read-only state, scheduled when the
285
+ * working state ends so the field stays locked for the shimmer wind-down.
286
+ * Non-`null` only while winding down.
287
+ */
288
+ #restoreTimer = null;
289
+
290
+ /** Set when the AI-fill announcement should be made on the next update. */
291
+ #announcePending = false;
292
+
293
+ /**
294
+ * Holds back the field's value assignments while working. Kept across
295
+ * working states, so a new assignment supersedes a still-queued one.
296
+ */
297
+ #valueDelay = null;
298
+
299
+ /**
300
+ * Stable badge id: generating it in render() would re-target the tooltip
301
+ * and popover on every re-render.
302
+ */
303
+ #badgeId = `vaadin-ai-field-marker-${generateUniqueId()}`;
304
+
305
+ constructor() {
306
+ super();
307
+
308
+ // The marker and its popover content live in the field's light DOM, so a
309
+ // click on the badge or inside the popover bubbles to the field host.
310
+ // Fields that open their overlay on any host click (date-picker,
311
+ // multi-select-combo-box) would act on it as if the field itself had been
312
+ // clicked. Keep marker clicks to the marker. The popover and tooltip bind
313
+ // their listeners on the badge, which is a descendant, so they still fire
314
+ // before this bubble-phase listener.
315
+ this.addEventListener('click', (event) => event.stopPropagation());
316
+
317
+ // Close the popover when focus moves on, e.g. by tabbing to the next
318
+ // field: a click-triggered popover only closes itself on outside pointer
319
+ // interaction or Esc, so popovers of several marked fields could pile up.
320
+ // Where focus ended up is read only once the transition has settled — mid
321
+ // transition the document has no focused element, which the popover
322
+ // overlay reads as focus not having left it (see
323
+ // OverlayFocusMixin._shouldRestoreFocus) and restores focus to the badge,
324
+ // stealing it from the input the user clicked.
325
+ this.addEventListener('focusout', () => {
326
+ setTimeout(() => {
327
+ if (!this.contains(getDeepActiveElement())) {
328
+ this.#closePopover();
329
+ }
330
+ });
331
+ });
332
+ }
333
+
334
+ /**
335
+ * Render into the light DOM instead of a shadow root: the themes can only
336
+ * reach a nested Vaadin component (the tooltip, the popover) there, since
337
+ * Aura selects components by tag name at document scope and has no way
338
+ * into another component's shadow root. The tooltip and popover target the
339
+ * badge by id, which resolves in the light-DOM scope shared by all three.
340
+ *
341
+ * @protected
342
+ * @override
343
+ */
344
+ createRenderRoot() {
345
+ return this;
346
+ }
347
+
348
+ /**
349
+ * The object used to localize this component. To change the default
350
+ * localization, replace this with an object that provides all properties, or
351
+ * just the individual properties you want to change.
352
+ *
353
+ * The object has the following JSON structure and default values:
354
+ *
355
+ * ```
356
+ * {
357
+ * // The message shown in the popover explaining the AI fill.
358
+ * message: 'This field value was modified by AI.',
359
+ * // The label of the revert control.
360
+ * revert: 'Revert Value',
361
+ * // The accessible label of the badge button and the popover dialog.
362
+ * badgeLabel: 'AI-provided value',
363
+ * // The tooltip text of the badge button.
364
+ * badgeTooltip: 'Field value modified by AI.\nClick for details'
365
+ * }
366
+ * ```
367
+ *
368
+ * @return {!Object}
369
+ */
370
+ get i18n() {
371
+ return super.i18n;
372
+ }
373
+
374
+ set i18n(value) {
375
+ super.i18n = value;
376
+ }
377
+
378
+ /**
379
+ * Override getter from `SlotStylesMixin` to insert the marker styles into
380
+ * the field's root node — the marker's own root node, since the marker is
381
+ * a child of the field — so the badge, popover and working-shimmer styles
382
+ * apply to the field.
383
+ *
384
+ * `SlotStylesMixin` inserts them as a `<style>` element rather than an
385
+ * adopted stylesheet, which matters here: the themable infrastructure
386
+ * replaces `adoptedStyleSheets` wholesale — on a Lumo stylesheet load or a
387
+ * theme switch — which, for a field nested in another component's shadow
388
+ * root, would silently drop the marker styles.
389
+ *
390
+ * @protected
391
+ * @override
392
+ * @return {string[]}
393
+ */
394
+ get slotStyles() {
395
+ return [aiFieldMarkerStyles.cssText];
396
+ }
397
+
398
+ /**
399
+ * Marks the parent field as AI-filled: injects the highlight + badge +
400
+ * popover into the field's shadow root, announces the change to screen
401
+ * readers, and associates the explanation with the field's input.
402
+ * Does nothing when the parent is not a field with a shadow root.
403
+ *
404
+ * @protected
405
+ * @override
406
+ */
407
+ connectedCallback() {
408
+ super.connectedCallback();
409
+
410
+ const parent = this.parentElement;
411
+ if (parent?.shadowRoot) {
412
+ this.#field = parent;
413
+ }
414
+
415
+ // Render now that the field is known: PolylitMixin's synchronous render on
416
+ // first connect ran before it was resolved, and on a reconnect no property
417
+ // change schedules an update. Requested even without a field, so that moving
418
+ // the marker to a parent that is not one clears the previous field's UI.
419
+ this.requestUpdate();
420
+
421
+ if (this.#field) {
422
+ this.#markField();
423
+ } else {
424
+ this.#markWhenUpgraded(parent);
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Removes the AI-filled annotation from the field the marker was attached
430
+ * to: clears the working state (restoring the field's client-side
431
+ * read-only state), the input description and the injected slot.
432
+ *
433
+ * @protected
434
+ * @override
435
+ */
436
+ disconnectedCallback() {
437
+ super.disconnectedCallback();
438
+
439
+ const field = this.#field;
440
+ if (!field) {
441
+ return;
442
+ }
443
+
444
+ this.#stopWorking(true);
445
+
446
+ if (this.#descNode) {
447
+ removeValuesFromAttribute(this.#describedElement, 'aria-describedby', this.#descNode.id);
448
+ this.#descNode.remove();
449
+ this.#descNode = null;
450
+ this.#describedElement = null;
451
+ }
452
+
453
+ // Remove the injected slot and styles unless another marker still uses them.
454
+ if (!field.querySelector(`:scope > ${AiFieldMarker.is}`)) {
455
+ field.shadowRoot.querySelector(`slot[name="${MARKER_SLOT}"]`)?.remove();
456
+ field.shadowRoot.querySelector(`style[${MARKER_STYLE_ATTRIBUTE}]`)?.remove();
457
+ }
458
+
459
+ this.#field = null;
460
+ this.#valueDelay = null;
461
+ }
462
+
463
+ /**
464
+ * @protected
465
+ * @override
466
+ */
467
+ updated(props) {
468
+ super.updated(props);
469
+
470
+ // Keep the hidden field description in sync with the current message.
471
+ if (props.has('__effectiveI18n') && this.#descNode) {
472
+ this.#descNode.textContent = this.__effectiveI18n.message;
473
+ }
474
+
475
+ const field = this.#field;
476
+ if (!field) {
477
+ return;
478
+ }
479
+
480
+ if (props.has('working')) {
481
+ if (this.working) {
482
+ this.#startWorking();
483
+ } else if (this.#lockedElements) {
484
+ this.#stopWorking();
485
+ // The fill landed: the marker now annotates the current value, so
486
+ // re-capture it for the revert event and announce the mark again.
487
+ this.#capturedValue = this.#annotatedValue();
488
+ this.#announcePending = true;
489
+ }
490
+ }
491
+
492
+ // Announce after the update so the announcement reflects a message set in
493
+ // the same batch as the append or the `working` toggle.
494
+ if (this.#announcePending && !this.working) {
495
+ this.#announcePending = false;
496
+ const { message } = this.__effectiveI18n;
497
+ const { label } = field;
498
+ announce(label ? `${label}: ${message}` : message);
499
+ }
500
+ }
501
+
502
+ /** @protected */
503
+ render() {
504
+ if (!this.#field) {
505
+ return nothing;
506
+ }
507
+
508
+ const { message, revert, badgeLabel, badgeTooltip } = this.__effectiveI18n;
509
+ // Safari leaves buttons out of the tab order unless they set a tabindex.
510
+ return html`
511
+ <button id="${this.#badgeId}" class="badge" type="button" tabindex="0" aria-label="${badgeLabel}"></button>
512
+ <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
+ >
522
+ <p class="message">${message}</p>
523
+ <div class="actions">
524
+ <button type="button" tabindex="0" @click="${this.#onRevert}">${revert}</button>
525
+ </div>
526
+ </vaadin-popover>
527
+ `;
528
+ }
529
+
530
+ /**
531
+ * Waits for the parent's custom element definition to load and marks it then,
532
+ * for a marker attached before the field was upgraded — at which point it had
533
+ * no shadow root to inject the marker into.
534
+ *
535
+ * @param {HTMLElement} parent
536
+ */
537
+ #markWhenUpgraded(parent) {
538
+ const tagName = parent?.localName;
539
+ if (!tagName || !tagName.includes('-') || customElements.get(tagName)) {
540
+ return;
541
+ }
542
+
543
+ customElements.whenDefined(tagName).then(() => {
544
+ // The marker may have been moved or removed while the field was loading,
545
+ // or already marked by a callback an earlier connect to the same parent
546
+ // left waiting.
547
+ if (!this.isConnected || this.#field || this.parentElement !== parent || !parent.shadowRoot) {
548
+ return;
549
+ }
550
+
551
+ this.#field = parent;
552
+ this.requestUpdate();
553
+ this.#markField();
554
+ });
555
+ }
556
+
557
+ /**
558
+ * Injects the marker into the resolved field and describes it as AI-filled.
559
+ */
560
+ #markField() {
561
+ const field = this.#field;
562
+
563
+ injectMarkerHostStyles(field);
564
+
565
+ // Create a slot for the marker element inside the field's own shadow
566
+ // root (unless a previous marker already left one) and assign the marker
567
+ // to it, so the marker renders although the field defines no such slot.
568
+ if (!field.shadowRoot.querySelector(`slot[name="${MARKER_SLOT}"]`)) {
569
+ const markerSlot = document.createElement('slot');
570
+ markerSlot.setAttribute('name', MARKER_SLOT);
571
+ field.shadowRoot.appendChild(markerSlot);
572
+ }
573
+ this.slot = MARKER_SLOT;
574
+
575
+ // Add a hidden description node in the field's light DOM (so its id
576
+ // resolves in the described element's scope) and append its id to that
577
+ // element's aria-describedby. Appending — rather than using
578
+ // aria-description, which a screen reader ignores when aria-describedby is
579
+ // present — lets the field's own helper/error description and the AI note
580
+ // both get read.
581
+ //
582
+ // `ariaTarget` is where the field puts its own descriptions, and is the
583
+ // only one of the three for group and composite fields, which expose
584
+ // neither an input nor a focus element.
585
+ const describedElement = field.ariaTarget || field.inputElement || field.focusElement;
586
+ if (describedElement) {
587
+ const descNode = document.createElement('span');
588
+ descNode.id = `ai-field-marker-${generateUniqueId()}`;
589
+ descNode.className = 'description sr-only';
590
+ descNode.textContent = this.__effectiveI18n.message;
591
+ // Insert before Lit's rendered content so the node stays outside the
592
+ // range Lit manages (and may clear) in the light-DOM render root.
593
+ this.insertBefore(descNode, this.firstChild);
594
+ addValuesToAttribute(describedElement, 'aria-describedby', descNode.id);
595
+ this.#descNode = descNode;
596
+ this.#describedElement = describedElement;
597
+ }
598
+
599
+ // Capture the AI-filled value so the revert event can carry it.
600
+ this.#capturedValue = this.#annotatedValue();
601
+
602
+ if (this.working) {
603
+ // Apply the working state directly: on a reconnect no `working`
604
+ // property change triggers updated(), which handles the first connect.
605
+ this.#startWorking();
606
+ } else {
607
+ this.#announcePending = true;
608
+ }
609
+ }
610
+
611
+ /**
612
+ * The field value the mark annotates. A value the AI has already set but
613
+ * that the working state still holds back counts as the annotated one, since
614
+ * it is the value the field ends up showing.
615
+ *
616
+ * @return {unknown} the annotated value, or `undefined` for a field that has
617
+ * no value at all
618
+ */
619
+ #annotatedValue() {
620
+ const field = this.#field;
621
+ return this.#valueDelay ? this.#valueDelay.latestValue : field.value;
622
+ }
623
+
624
+ /**
625
+ * Enters the "AI is working" state: shows the shimmer and makes the field
626
+ * read-only on the client so the user cannot edit a value the AI is about
627
+ * to overwrite. Idempotent — keeps the state captured on entry.
628
+ */
629
+ #startWorking() {
630
+ const field = this.#field;
631
+ if (!field || (this.#lockedElements && this.#restoreTimer == null)) {
632
+ return;
633
+ }
634
+
635
+ this.#valueDelay ??= new DelayedFieldValue(field, HALF_WORKING_SLIDE_MS);
636
+ this.#valueDelay.install();
637
+
638
+ if (this.#restoreTimer != null) {
639
+ // The previous working state is still winding down. Cancel its restore
640
+ // and keep the read-only state it captured: the elements are locked right
641
+ // now, so capturing again would take the lock itself as the original.
642
+ clearTimeout(this.#restoreTimer);
643
+ this.#restoreTimer = null;
644
+ } else {
645
+ // A composite field does not propagate `readonly` to its inputs, so they
646
+ // are locked (and restored) individually alongside the field. Recognized
647
+ // by the `inputs` array rather than by tag name, which also covers a
648
+ // composite field shipped under its own tag name.
649
+ const locked = [field, ...(Array.isArray(field.inputs) ? field.inputs : [])];
650
+ this.#lockedElements = locked.map((element) => {
651
+ // A composite field also accepts native inputs, which spell the
652
+ // property `readOnly`.
653
+ const property = 'readonly' in element ? 'readonly' : 'readOnly';
654
+ return { element, property, value: element[property] };
655
+ });
656
+ }
657
+
658
+ field.setAttribute('ai-working', '');
659
+ // Expose the working state to assistive technology on the same element
660
+ // that carries the AI description: the shimmer alone is only visual.
661
+ this.#describedElement?.setAttribute('aria-busy', 'true');
662
+ this.#lockedElements.forEach(({ element, property }) => {
663
+ element[property] = true;
664
+ });
665
+ }
666
+
667
+ /**
668
+ * Leaves the "AI is working" state: removes the shimmer and restores the
669
+ * field's previous client-side read-only state. A no-op when not working.
670
+ *
671
+ * @param {boolean} immediate restore the read-only state right away instead
672
+ * of after the shimmer wind-down (used on disconnect)
673
+ */
674
+ #stopWorking(immediate = false) {
675
+ const field = this.#field;
676
+ if (!field) {
677
+ return;
678
+ }
679
+
680
+ if (this.#restoreTimer != null) {
681
+ // Already winding down. Finish it now when the marker is going away, so
682
+ // the restore cannot overwrite a read-only state set after this point
683
+ // and a value still queued from the working state cannot land after it.
684
+ if (immediate) {
685
+ this.#restoreLockedElements();
686
+ }
687
+ return;
688
+ }
689
+
690
+ if (!this.#lockedElements) {
691
+ return;
692
+ }
693
+
694
+ field.removeAttribute('ai-working');
695
+ this.#describedElement?.removeAttribute('aria-busy');
696
+
697
+ // The value hold-back stays installed for the wind-down: a queued value
698
+ // still lands on its own deadline, and a value the host sets before the
699
+ // wind-down finishes supersedes a queued one instead of being overwritten
700
+ // by it when its deadline passes. The restore then lifts the hold-back.
701
+ if (immediate) {
702
+ this.#restoreLockedElements();
703
+ } else {
704
+ this.#restoreTimer = setTimeout(() => this.#restoreLockedElements(), HALF_WORKING_SLIDE_MS);
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Restores the read-only state captured when the working state was entered
710
+ * and stops holding back the field's value assignments, applying a value
711
+ * still queued at this point right away — rather than on its deadline, after
712
+ * the marker stopped controlling the field.
713
+ */
714
+ #restoreLockedElements() {
715
+ clearTimeout(this.#restoreTimer);
716
+ this.#restoreTimer = null;
717
+
718
+ this.#valueDelay.uninstall();
719
+
720
+ const locked = this.#lockedElements;
721
+ this.#lockedElements = null;
722
+ locked.forEach(({ element, property, value }) => {
723
+ element[property] = value;
724
+ });
725
+ }
726
+
727
+ /** Closes the marker's popover, if rendered. */
728
+ #closePopover() {
729
+ const popover = this.querySelector(':scope > vaadin-popover');
730
+ if (popover) {
731
+ popover.opened = false;
732
+ }
733
+ }
734
+
735
+ #onRevert() {
736
+ // Return focus to the field before closing the popover. The popover
737
+ // targets the badge for focus restoration, but the host may remove the
738
+ // marker on revert, which would drop focus to the body. Moving focus to
739
+ // the field first makes the overlay skip its own restore — it only
740
+ // restores while focus is still inside the overlay (see
741
+ // OverlayFocusMixin._shouldRestoreFocus).
742
+ //
743
+ // Focus the field's own focusable element rather than calling focus() on
744
+ // the host: a host focus() can carry component-specific semantics that a
745
+ // revert must not trigger — date-picker opens its overlay on focus while
746
+ // it has no usable text input (fullscreen, iOS, or no i18n.parseDate).
747
+ //
748
+ // The revert control can be activated by pointer as well as by keyboard,
749
+ // so the focus ring is left to the current interaction modality instead of
750
+ // being forced on, which is what a bare focus() does on a Vaadin field.
751
+ const field = this.#field;
752
+ if (field) {
753
+ const focusTarget = field.focusElement || field.inputElement || getTabbableElements(field)[0] || field;
754
+ focusTarget.focus({ focusVisible: isKeyboardActive() });
755
+ }
756
+
757
+ this.#closePopover();
758
+
759
+ if (field) {
760
+ field.dispatchEvent(
761
+ new CustomEvent('ai-field-revert', {
762
+ bubbles: true,
763
+ composed: true,
764
+ detail: { value: this.#capturedValue },
765
+ }),
766
+ );
767
+ }
768
+ }
769
+ }
770
+
771
+ defineCustomElement(AiFieldMarker);
772
+
773
+ export { AiFieldMarker };
@@ -155,7 +155,7 @@ export class FieldHighlighterController {
155
155
  *
156
156
  * @customElement vaadin-field-highlighter
157
157
  */
158
- export class FieldHighlighter extends HTMLElement {
158
+ class FieldHighlighter extends HTMLElement {
159
159
  static get is() {
160
160
  return 'vaadin-field-highlighter';
161
161
  }
@@ -194,3 +194,5 @@ export class FieldHighlighter extends HTMLElement {
194
194
  }
195
195
 
196
196
  defineCustomElement(FieldHighlighter);
197
+
198
+ export { FieldHighlighter };
@@ -17,7 +17,7 @@ import { fieldOutlineStyles } from './styles/vaadin-field-outline-base-styles.js
17
17
  * @extends HTMLElement
18
18
  * @private
19
19
  */
20
- export class FieldOutline extends ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMixin(LitElement)))) {
20
+ class FieldOutline extends ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMixin(LitElement)))) {
21
21
  static get is() {
22
22
  return 'vaadin-field-outline';
23
23
  }
@@ -72,3 +72,5 @@ export class FieldOutline extends ThemableMixin(DirMixin(PolylitMixin(LumoInject
72
72
  }
73
73
 
74
74
  defineCustomElement(FieldOutline);
75
+
76
+ export { FieldOutline };
@@ -18,7 +18,7 @@ import { userTagStyles } from './styles/vaadin-user-tag-base-styles.js';
18
18
  * @extends HTMLElement
19
19
  * @private
20
20
  */
21
- export class UserTag extends ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMixin(LitElement)))) {
21
+ class UserTag extends ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMixin(LitElement)))) {
22
22
  static get is() {
23
23
  return 'vaadin-user-tag';
24
24
  }
@@ -94,3 +94,5 @@ export class UserTag extends ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMi
94
94
  }
95
95
 
96
96
  defineCustomElement(UserTag);
97
+
98
+ export { UserTag };
@@ -28,7 +28,7 @@ const listenOnce = (elem, type) => {
28
28
  * @extends HTMLElement
29
29
  * @private
30
30
  */
31
- export class UserTags extends PolylitMixin(LitElement) {
31
+ class UserTags extends PolylitMixin(LitElement) {
32
32
  static get is() {
33
33
  return 'vaadin-user-tags';
34
34
  }
@@ -442,3 +442,5 @@ export class UserTags extends PolylitMixin(LitElement) {
442
442
  }
443
443
 
444
444
  defineCustomElement(UserTags);
445
+
446
+ export { UserTags };