@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 +3 -0
- package/index.js +2 -0
- package/input/vi-input.d.ts.map +1 -1
- package/input/vi-input.js +1 -215
- package/package.json +14 -2
- package/validity-mixin-BjmXwgWk.js +216 -0
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';
|
package/input/vi-input.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vi-input.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/vi-input.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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.
|
|
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": "
|
|
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 };
|