@vialiq/web-components 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -8,4 +8,7 @@ export type { InputType } from './input/vi-input.js';
8
8
  export { ViIcon } from './icons/vi-icon.js';
9
9
  export { registerIcons, getIcon } from './icons/registry.js';
10
10
  export type { SvgIconDef } from './icons/registry.js';
11
+ export { ViRadio } from './radio/vi-radio.js';
12
+ export { ViRadioGroup } from './radio/vi-radio-group.js';
13
+ export type { RadioGroupOrientation } from './radio/vi-radio-group.js';
11
14
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -3,3 +3,5 @@ export { ViButton } from './button/vi-button.js';
3
3
  export { ViInput } from './input/vi-input.js';
4
4
  export { ViIcon } from './icons/vi-icon.js';
5
5
  export { getIcon, registerIcons } from './icons/registry.js';
6
+ export { ViRadio } from './radio/vi-radio.js';
7
+ export { ViRadioGroup } from './radio/vi-radio-group.js';
@@ -1 +1 @@
1
- {"version":3,"file":"vi-input.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/vi-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,KAAK,CAAC;AAGb,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIlD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,KAAK,GACL,KAAK,GACL,QAAQ,CAAC;;AAEb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBACa,OAAQ,SAAQ,YAAwC;IACnE,MAAM,CAAC,cAAc,UAAQ;IAC7B,OAAgB,MAAM,0BAEpB;IAEF,SAAS,CAAC,QAAQ,CAAC,UAAU,mBAA0B;IAEvD,cAAuB,iBAAiB,IAAI,gBAAgB,GAAG,IAAI,CAElE;IAI4B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAa;IAC5B,QAAQ,CAAC,QAAQ,UAAS;IAC1D,QAAQ,CAAC,eAAe,SAAM;IAI1C,uFAAuF;IAC5C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAU;IAE7E,qCAAqC;IACzB,QAAQ,CAAC,WAAW,SAAM;IAEtC,yDAAyD;IAC7C,QAAQ,CAAC,IAAI,SAAM;IAE/B,wEAAwE;IAC5D,QAAQ,CAAC,KAAK,SAAM;IAEhC,uEAAuE;IAC3B,QAAQ,CAAC,QAAQ,UAAS;IAEtE,oEAAoE;IACxB,QAAQ,CAAC,QAAQ,UAAS;IAOtE,SAAS,CAAC,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAmC7C,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAU/C,yEAAyE;IACzE,iBAAiB,IAAI,IAAI;IAMzB,6EAA6E;IAC7E,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAM7C,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,SAAS;IAejB,OAAO,KAAK,cAAc,GAIzB;IAED,OAAO,KAAK,kBAAkB,GAgB7B;IAEQ,MAAM,IAAI,cAAc;CAkClC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,UAAU,EAAE,OAAO,CAAC;KACrB;CACF"}
1
+ {"version":3,"file":"vi-input.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/vi-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,KAAK,CAAC;AAGb,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIlD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,KAAK,GACL,KAAK,GACL,QAAQ,CAAC;;AAEb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBACa,OAAQ,SAAQ,YAAwC;IACnE,MAAM,CAAC,cAAc,UAAQ;IAC7B,OAAgB,MAAM,0BAEpB;IAEF,SAAS,CAAC,QAAQ,CAAC,UAAU,mBAA0B;IAEvD,cAAuB,iBAAiB,IAAI,gBAAgB,GAAG,IAAI,CAElE;IAI4B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAa;IAC5B,QAAQ,CAAC,QAAQ,UAAS;IAC1D,QAAQ,CAAC,eAAe,SAAM;IAI1C,uFAAuF;IAC5C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAU;IAE7E,qCAAqC;IACzB,QAAQ,CAAC,WAAW,SAAM;IAEtC,yDAAyD;IAC7C,QAAQ,CAAC,IAAI,SAAM;IAE/B,wEAAwE;IAC5D,QAAQ,CAAC,KAAK,SAAM;IAEhC,uEAAuE;IAC3B,QAAQ,CAAC,QAAQ,UAAS;IAEtE,oEAAoE;IACxB,QAAQ,CAAC,QAAQ,UAAS;IAOtE,SAAS,CAAC,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAmC7C,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAU/C,yEAAyE;IACzE,iBAAiB,IAAI,IAAI;IAMzB,6EAA6E;IAC7E,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAM7C,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,SAAS;IAejB,OAAO,KAAK,cAAc,GAIzB;IAED,OAAO,KAAK,kBAAkB,GAgB7B;IAEQ,MAAM,IAAI,cAAc;CAkClC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,UAAU,EAAE,OAAO,CAAC;KACrB;CACF"}
package/input/vi-input.js CHANGED
@@ -1,224 +1,10 @@
1
1
  import { unsafeCSS, css, html } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import { F as FocusableMixin } from '../focusable-mixin-CmxOyPX5.js';
4
+ import { V as ValidityMixin } from '../validity-mixin-BjmXwgWk.js';
4
5
  import { V as ViElement } from '../vi-element-C6GfDPs3.js';
5
6
  import { ifDefined } from 'lit/directives/if-defined.js';
6
7
 
7
- /**
8
- * ValidityMixin
9
- *
10
- * Adds the standard form validation API (`checkValidity`, `reportValidity`,
11
- * `setCustomValidity`) to any form-associated Lit element.
12
- *
13
- * Backed by the native `ElementInternals` API so the component participates
14
- * in `HTMLFormElement` constraint validation, `.elements`, and browser
15
- * validation UI — exactly like a native `<input>`.
16
- *
17
- * ─────────────────────────────────────────────────────────────────────────
18
- * MINIMUM SUBCLASS REQUIREMENTS
19
- * ─────────────────────────────────────────────────────────────────────────
20
- *
21
- * 1. Declare static formAssociated = true;
22
- * Makes the browser register this element as a form participant.
23
- *
24
- * 2. Attach ElementInternals:
25
- * protected readonly _internals = this.attachInternals();
26
- * Must be a field initializer (runs after super() in the constructor).
27
- *
28
- * 3. Declare reactive properties (MUST be @property so Lit tracks changes):
29
- * @property({ reflect: true }) accessor status: ControlStatus = 'default';
30
- * @property({ type: Boolean, reflect: true }) accessor required = false;
31
- * @property() accessor validityMessage = '';
32
- * @property() accessor value = '';
33
- *
34
- * 4. Sync value to internals on every value change:
35
- * override updated(changed: PropertyValues): void {
36
- * super.updated(changed);
37
- * if (changed.has('value')) {
38
- * this._internals.setFormValue(this.value);
39
- * }
40
- * }
41
- *
42
- * 5. Handle form reset:
43
- * formResetCallback(): void {
44
- * this.value = this.getAttribute('value') ?? '';
45
- * this.status = 'default';
46
- * this.validityMessage = '';
47
- * }
48
- *
49
- * 6. Handle fieldset/form disable:
50
- * formDisabledCallback(disabled: boolean): void {
51
- * this.disabled = disabled;
52
- * }
53
- *
54
- * ─────────────────────────────────────────────────────────────────────────
55
- * OVERRIDE _testValidity() FOR CUSTOM CONSTRAINTS
56
- * ─────────────────────────────────────────────────────────────────────────
57
- *
58
- * protected override _testValidity(): Partial<ValidityStateFlags> {
59
- * if (this.required && !this.value) return { valueMissing: true };
60
- * return {};
61
- * }
62
- *
63
- * Chain constraints for components with multiple rules (e.g. vi-input[type="number"]):
64
- *
65
- * protected override _testValidity(): Partial<ValidityStateFlags> {
66
- * if (this.required && !this.value) return { valueMissing: true };
67
- * if (this.minlength && this.value.length < this.minlength)
68
- * return { tooShort: true };
69
- * if (this.maxlength && this.value.length > this.maxlength)
70
- * return { tooLong: true };
71
- * return {};
72
- * }
73
- *
74
- * ─────────────────────────────────────────────────────────────────────────
75
- * FULL USAGE EXAMPLE — vi-input
76
- * ─────────────────────────────────────────────────────────────────────────
77
- *
78
- * import { property } from 'lit/decorators.js';
79
- * import { ValidityMixin } from '../base/validity-mixin.js';
80
- * import { FocusableMixin } from '../base/focusable-mixin.js';
81
- * import { ViElement } from '../base/vi-element.js';
82
- *
83
- * @customElement('vi-input')
84
- * export class ViInput extends ValidityMixin(FocusableMixin(ViElement)) {
85
- * static override formAssociated = true;
86
- * protected readonly _internals = this.attachInternals();
87
- *
88
- * @property({ reflect: true }) accessor status: ControlStatus = 'default';
89
- * @property({ type: Boolean, reflect: true }) accessor required = false;
90
- * @property() accessor validityMessage = '';
91
- * @property() accessor value = '';
92
- *
93
- * protected override _testValidity(): Partial<ValidityStateFlags> {
94
- * if (this.required && !this.value) return { valueMissing: true };
95
- * return {};
96
- * }
97
- *
98
- * override updated(changed: PropertyValues): void {
99
- * super.updated(changed);
100
- * if (changed.has('value')) this._internals.setFormValue(this.value);
101
- * }
102
- *
103
- * formResetCallback(): void {
104
- * this.value = this.getAttribute('value') ?? '';
105
- * this.status = 'default';
106
- * this.validityMessage = '';
107
- * }
108
- *
109
- * formDisabledCallback(disabled: boolean): void {
110
- * this.disabled = disabled;
111
- * }
112
- * }
113
- *
114
- * ─────────────────────────────────────────────────────────────────────────
115
- * INVALID EVENT BEHAVIOUR
116
- * ─────────────────────────────────────────────────────────────────────────
117
- *
118
- * checkValidity() fires a cancelable 'invalid' event when validation fails.
119
- * The event does NOT bubble (matches native form element behaviour).
120
- * Consumer can suppress the default UI by calling event.preventDefault().
121
- *
122
- * Example:
123
- * myInput.addEventListener('invalid', (e) => {
124
- * e.preventDefault(); // suppress browser tooltip
125
- * showMyCustomError(myInput.validityMessage);
126
- * });
127
- *
128
- * ─────────────────────────────────────────────────────────────────────────
129
- * MIXIN COMPOSITION ORDER
130
- * ─────────────────────────────────────────────────────────────────────────
131
- *
132
- * ValidityMixin should wrap FocusableMixin (outermost):
133
- * ValidityMixin(FocusableMixin(ViElement))
134
- *
135
- * Reason: ValidityMixin only adds methods (checkValidity etc.). It does not
136
- * touch shadowRootOptions or focus delegation, so order has no side-effects.
137
- * Convention is: functionality mixins wrap infrastructure mixins.
138
- */ function ValidityMixin(Base) {
139
- class ValidityMixinClass extends Base {
140
- /**
141
- * Base implementation — always valid.
142
- * Subclass overrides this to return flags like { valueMissing: true }.
143
- */ _testValidity() {
144
- return {};
145
- }
146
- checkValidity() {
147
- const flags = this._testValidity();
148
- const isValid = !Object.values(flags).some(Boolean);
149
- // Sync to ElementInternals so native form API (formElement.checkValidity,
150
- // :invalid CSS pseudo-class, browser tooltip) reflects the same state.
151
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
- const internals = this._internals;
153
- if (internals) {
154
- if (isValid) {
155
- internals.setValidity({}, '');
156
- } else {
157
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
- const msg = this.validityMessage || 'Invalid value';
159
- internals.setValidity(flags, msg);
160
- }
161
- }
162
- if (isValid) {
163
- // Only clear status if it was previously set to 'invalid' by the constraint
164
- // API. Do not override an explicit 'valid' set by the parent/framework.
165
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
- if (this.status === 'invalid') {
167
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
- this.status = 'default';
169
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
- this.validityMessage = '';
171
- }
172
- } else {
173
- // Fire cancelable 'invalid' — mirrors native form element behaviour.
174
- // Cancelable so consumers can suppress browser tooltip and show their own.
175
- // bubbles: false — matches native <input> 'invalid' event.
176
- // composed: false — stays within the document, does not cross shadow boundaries.
177
- const proceed = this.dispatchEvent(new Event('invalid', {
178
- bubbles: false,
179
- cancelable: true,
180
- composed: false
181
- }));
182
- if (proceed) {
183
- // Only set status='invalid' if the event was not cancelled.
184
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
- this.status = 'invalid';
186
- }
187
- }
188
- return isValid;
189
- }
190
- reportValidity() {
191
- // Prefer ElementInternals.reportValidity() — it triggers native browser UI
192
- // (tooltip near the field). Falls back to checkValidity() if internals not set.
193
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
- const internals = this._internals;
195
- if (internals) {
196
- // Must sync validity state first so the browser has something to show.
197
- this.checkValidity();
198
- return internals.reportValidity();
199
- }
200
- return this.checkValidity();
201
- }
202
- setCustomValidity(message) {
203
- const hasError = Boolean(message);
204
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
- this.status = hasError ? 'invalid' : 'default';
206
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
207
- this.validityMessage = message;
208
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
- const internals = this._internals;
210
- if (internals) {
211
- internals.setValidity(hasError ? {
212
- customError: true
213
- } : {}, hasError ? message : '');
214
- }
215
- }
216
- }
217
- // Cast required: TypeScript cannot reconcile LitElement's private fields
218
- // with the anonymous class return type. Same pattern as FocusableMixin.
219
- return ValidityMixinClass;
220
- }
221
-
222
8
  /**
223
9
  * ifNonEmpty — conditional attribute directive
224
10
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vialiq/web-components",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,10 +35,22 @@
35
35
  "./input": {
36
36
  "types": "./input/vi-input.d.ts",
37
37
  "default": "./input/vi-input.js"
38
+ },
39
+ "./radio/vi-radio": {
40
+ "types": "./radio/vi-radio.d.ts",
41
+ "default": "./radio/vi-radio.js"
42
+ },
43
+ "./radio/vi-radio-group": {
44
+ "types": "./radio/vi-radio-group.d.ts",
45
+ "default": "./radio/vi-radio-group.js"
46
+ },
47
+ "./radio": {
48
+ "types": "./radio/index.d.ts",
49
+ "default": "./radio/index.js"
38
50
  }
39
51
  },
40
52
  "peerDependencies": {
41
- "@vialiq/flux-ui": ">=0.0.4 <0.1.0",
53
+ "@vialiq/flux-ui": "^0.2.0",
42
54
  "@vialiq/icons": ">=0.0.3 <0.1.0",
43
55
  "lit": "^3.0.0"
44
56
  },
@@ -0,0 +1,216 @@
1
+ /**
2
+ * ValidityMixin
3
+ *
4
+ * Adds the standard form validation API (`checkValidity`, `reportValidity`,
5
+ * `setCustomValidity`) to any form-associated Lit element.
6
+ *
7
+ * Backed by the native `ElementInternals` API so the component participates
8
+ * in `HTMLFormElement` constraint validation, `.elements`, and browser
9
+ * validation UI — exactly like a native `<input>`.
10
+ *
11
+ * ─────────────────────────────────────────────────────────────────────────
12
+ * MINIMUM SUBCLASS REQUIREMENTS
13
+ * ─────────────────────────────────────────────────────────────────────────
14
+ *
15
+ * 1. Declare static formAssociated = true;
16
+ * Makes the browser register this element as a form participant.
17
+ *
18
+ * 2. Attach ElementInternals:
19
+ * protected readonly _internals = this.attachInternals();
20
+ * Must be a field initializer (runs after super() in the constructor).
21
+ *
22
+ * 3. Declare reactive properties (MUST be @property so Lit tracks changes):
23
+ * @property({ reflect: true }) accessor status: ControlStatus = 'default';
24
+ * @property({ type: Boolean, reflect: true }) accessor required = false;
25
+ * @property() accessor validityMessage = '';
26
+ * @property() accessor value = '';
27
+ *
28
+ * 4. Sync value to internals on every value change:
29
+ * override updated(changed: PropertyValues): void {
30
+ * super.updated(changed);
31
+ * if (changed.has('value')) {
32
+ * this._internals.setFormValue(this.value);
33
+ * }
34
+ * }
35
+ *
36
+ * 5. Handle form reset:
37
+ * formResetCallback(): void {
38
+ * this.value = this.getAttribute('value') ?? '';
39
+ * this.status = 'default';
40
+ * this.validityMessage = '';
41
+ * }
42
+ *
43
+ * 6. Handle fieldset/form disable:
44
+ * formDisabledCallback(disabled: boolean): void {
45
+ * this.disabled = disabled;
46
+ * }
47
+ *
48
+ * ─────────────────────────────────────────────────────────────────────────
49
+ * OVERRIDE _testValidity() FOR CUSTOM CONSTRAINTS
50
+ * ─────────────────────────────────────────────────────────────────────────
51
+ *
52
+ * protected override _testValidity(): Partial<ValidityStateFlags> {
53
+ * if (this.required && !this.value) return { valueMissing: true };
54
+ * return {};
55
+ * }
56
+ *
57
+ * Chain constraints for components with multiple rules (e.g. vi-input[type="number"]):
58
+ *
59
+ * protected override _testValidity(): Partial<ValidityStateFlags> {
60
+ * if (this.required && !this.value) return { valueMissing: true };
61
+ * if (this.minlength && this.value.length < this.minlength)
62
+ * return { tooShort: true };
63
+ * if (this.maxlength && this.value.length > this.maxlength)
64
+ * return { tooLong: true };
65
+ * return {};
66
+ * }
67
+ *
68
+ * ─────────────────────────────────────────────────────────────────────────
69
+ * FULL USAGE EXAMPLE — vi-input
70
+ * ─────────────────────────────────────────────────────────────────────────
71
+ *
72
+ * import { property } from 'lit/decorators.js';
73
+ * import { ValidityMixin } from '../base/validity-mixin.js';
74
+ * import { FocusableMixin } from '../base/focusable-mixin.js';
75
+ * import { ViElement } from '../base/vi-element.js';
76
+ *
77
+ * @customElement('vi-input')
78
+ * export class ViInput extends ValidityMixin(FocusableMixin(ViElement)) {
79
+ * static override formAssociated = true;
80
+ * protected readonly _internals = this.attachInternals();
81
+ *
82
+ * @property({ reflect: true }) accessor status: ControlStatus = 'default';
83
+ * @property({ type: Boolean, reflect: true }) accessor required = false;
84
+ * @property() accessor validityMessage = '';
85
+ * @property() accessor value = '';
86
+ *
87
+ * protected override _testValidity(): Partial<ValidityStateFlags> {
88
+ * if (this.required && !this.value) return { valueMissing: true };
89
+ * return {};
90
+ * }
91
+ *
92
+ * override updated(changed: PropertyValues): void {
93
+ * super.updated(changed);
94
+ * if (changed.has('value')) this._internals.setFormValue(this.value);
95
+ * }
96
+ *
97
+ * formResetCallback(): void {
98
+ * this.value = this.getAttribute('value') ?? '';
99
+ * this.status = 'default';
100
+ * this.validityMessage = '';
101
+ * }
102
+ *
103
+ * formDisabledCallback(disabled: boolean): void {
104
+ * this.disabled = disabled;
105
+ * }
106
+ * }
107
+ *
108
+ * ─────────────────────────────────────────────────────────────────────────
109
+ * INVALID EVENT BEHAVIOUR
110
+ * ─────────────────────────────────────────────────────────────────────────
111
+ *
112
+ * checkValidity() fires a cancelable 'invalid' event when validation fails.
113
+ * The event does NOT bubble (matches native form element behaviour).
114
+ * Consumer can suppress the default UI by calling event.preventDefault().
115
+ *
116
+ * Example:
117
+ * myInput.addEventListener('invalid', (e) => {
118
+ * e.preventDefault(); // suppress browser tooltip
119
+ * showMyCustomError(myInput.validityMessage);
120
+ * });
121
+ *
122
+ * ─────────────────────────────────────────────────────────────────────────
123
+ * MIXIN COMPOSITION ORDER
124
+ * ─────────────────────────────────────────────────────────────────────────
125
+ *
126
+ * ValidityMixin should wrap FocusableMixin (outermost):
127
+ * ValidityMixin(FocusableMixin(ViElement))
128
+ *
129
+ * Reason: ValidityMixin only adds methods (checkValidity etc.). It does not
130
+ * touch shadowRootOptions or focus delegation, so order has no side-effects.
131
+ * Convention is: functionality mixins wrap infrastructure mixins.
132
+ */ function ValidityMixin(Base) {
133
+ class ValidityMixinClass extends Base {
134
+ /**
135
+ * Base implementation — always valid.
136
+ * Subclass overrides this to return flags like { valueMissing: true }.
137
+ */ _testValidity() {
138
+ return {};
139
+ }
140
+ checkValidity() {
141
+ const flags = this._testValidity();
142
+ const isValid = !Object.values(flags).some(Boolean);
143
+ // Sync to ElementInternals so native form API (formElement.checkValidity,
144
+ // :invalid CSS pseudo-class, browser tooltip) reflects the same state.
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ const internals = this._internals;
147
+ if (internals) {
148
+ if (isValid) {
149
+ internals.setValidity({}, '');
150
+ } else {
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ const msg = this.validityMessage || 'Invalid value';
153
+ internals.setValidity(flags, msg);
154
+ }
155
+ }
156
+ if (isValid) {
157
+ // Only clear status if it was previously set to 'invalid' by the constraint
158
+ // API. Do not override an explicit 'valid' set by the parent/framework.
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ if (this.status === 'invalid') {
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ this.status = 'default';
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ this.validityMessage = '';
165
+ }
166
+ } else {
167
+ // Fire cancelable 'invalid' — mirrors native form element behaviour.
168
+ // Cancelable so consumers can suppress browser tooltip and show their own.
169
+ // bubbles: false — matches native <input> 'invalid' event.
170
+ // composed: false — stays within the document, does not cross shadow boundaries.
171
+ const proceed = this.dispatchEvent(new Event('invalid', {
172
+ bubbles: false,
173
+ cancelable: true,
174
+ composed: false
175
+ }));
176
+ if (proceed) {
177
+ // Only set status='invalid' if the event was not cancelled.
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ this.status = 'invalid';
180
+ }
181
+ }
182
+ return isValid;
183
+ }
184
+ reportValidity() {
185
+ // Prefer ElementInternals.reportValidity() — it triggers native browser UI
186
+ // (tooltip near the field). Falls back to checkValidity() if internals not set.
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ const internals = this._internals;
189
+ if (internals) {
190
+ // Must sync validity state first so the browser has something to show.
191
+ this.checkValidity();
192
+ return internals.reportValidity();
193
+ }
194
+ return this.checkValidity();
195
+ }
196
+ setCustomValidity(message) {
197
+ const hasError = Boolean(message);
198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
+ this.status = hasError ? 'invalid' : 'default';
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ this.validityMessage = message;
202
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
+ const internals = this._internals;
204
+ if (internals) {
205
+ internals.setValidity(hasError ? {
206
+ customError: true
207
+ } : {}, hasError ? message : '');
208
+ }
209
+ }
210
+ }
211
+ // Cast required: TypeScript cannot reconcile LitElement's private fields
212
+ // with the anonymous class return type. Same pattern as FocusableMixin.
213
+ return ValidityMixinClass;
214
+ }
215
+
216
+ export { ValidityMixin as V };