@vialiq/web-components 0.2.0 → 0.4.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/button/vi-button.js +1 -1
- package/index.d.ts +7 -1
- package/index.js +3 -0
- package/input/index.d.ts +1 -1
- package/input/index.d.ts.map +1 -1
- package/input/vi-input.d.ts +3 -0
- package/input/vi-input.d.ts.map +1 -1
- package/input/vi-input.js +23 -222
- package/package.json +14 -2
- package/validity-mixin-BjmXwgWk.js +216 -0
package/button/vi-button.js
CHANGED
|
@@ -3,7 +3,7 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|
|
3
3
|
import { F as FocusableMixin } from '../focusable-mixin-CmxOyPX5.js';
|
|
4
4
|
import { V as ViElement } from '../vi-element-C6GfDPs3.js';
|
|
5
5
|
|
|
6
|
-
const buttonStyles = "@charset \"UTF-8\";@layer reset,components,utilities;@layer components{.button{display:inline-flex;align-items:center;justify-content:center;gap:var(--vi-spacing-xs, 8px);border:var(--vi-border-width-thin, 1px) solid transparent;border-radius:var(--vi-button-shape-border-radius, var(--vi-border-radius-md, 4px));padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px)) var(--vi-button-spacing-padding-inline, var(--vi-spacing-md, 24px));font-size:var(--vi-button-typography-font-size, var(--vi-font-size-base, 16px));font-weight:var(--vi-button-typography-font-weight, var(--vi-font-weight-semibold, 600));line-height:var(--vi-line-height-tight, 1.2);cursor:pointer;-webkit-user-select:none;user-select:none;transition:opacity var(--vi-button-effect-transition-duration, .16s) ease,box-shadow var(--vi-button-effect-transition-duration, .16s) ease,transform var(--vi-button-effect-transition-duration, .16s) ease}}:host{display:inline-block}:host([size=xs]){--vi-button-spacing-padding-block: 2px;--vi-button-spacing-padding-inline: 8px;--vi-button-typography-font-size: var(--vi-font-size-xs, 12px)}:host([size=sm]){--vi-button-spacing-padding-block: 4px;--vi-button-spacing-padding-inline: 12px;--vi-button-typography-font-size: var(--vi-font-size-sm, 14px)}:host([size=lg]){--vi-button-spacing-padding-block: 12px;--vi-button-spacing-padding-inline: 24px;--vi-button-typography-font-size: var(--vi-font-size-lg, 18px)}:host([full-width]){display:block}:host([full-width]) .button{width:100%}:host([icon-only]) .button{aspect-ratio:1/1;padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px));justify-content:center}:host([icon-only]) .label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host([variant=primary]) .button{background-color:var(--vi-button-surface-primary-background-color, var(--vi-color-primary, #3676d0));color:var(--vi-button-surface-primary-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=secondary]) .button{background-color:var(--vi-button-surface-secondary-background-color, var(--vi-color-secondary, #f0f4f8));color:var(--vi-button-surface-secondary-text-color, var(--vi-color-foreground, #111827));border-color:var(--vi-color-border, #e5e7eb)}:host([variant=danger]) .button{background-color:var(--vi-button-surface-danger-background-color, var(--vi-color-error, #ef4444));color:var(--vi-button-surface-danger-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=success]) .button{background-color:var(--vi-button-surface-success-background-color, var(--vi-color-success, #489167));color:var(--vi-button-surface-success-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=info]) .button{background-color:var(--vi-button-surface-info-background-color, var(--vi-color-info, #3676d0));color:var(--vi-button-surface-info-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=ghost]) .button{background-color:var(--vi-button-surface-ghost-background-color, transparent);color:var(--vi-button-surface-ghost-text-color, var(--vi-color-primary, #3676d0))}:host([disabled]) .button,.button:disabled{opacity:.6;cursor:not-allowed}.button:not(:disabled){box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08))}.button:hover:not(:disabled){opacity:.92;box-shadow:var(--vi-button-effect-shadow-hover, inset 0 1px 0 rgba(255, 255, 255, .18), 0 4px 8px rgba(0, 0, 0, .22), 0 2px 4px rgba(0, 0, 0, .1));transform:translateY(-1px)}.button:active:not(:disabled){box-shadow:var(--vi-button-effect-shadow-pressed, inset 0 2px 4px rgba(0, 0, 0, .22), 0 1px 1px rgba(0, 0, 0, .06));transform:translateY(1px);opacity:1}:host([variant=ghost]) .button:hover:not(:disabled){background-color:#0000000a;text-decoration:underline;opacity:1}:host([variant=ghost]) .button:not(:disabled){box-shadow:none}:host([variant=ghost]) .button:active:not(:disabled){box-shadow:inset 0 1px 3px #00000024;transform:translateY(0)}.button:focus-visible:not(:disabled){outline:var(--vi-border-width-base, 2px) solid var(--vi-button-focus-ring-color, var(--vi-color-primary, #3676d0));outline-offset:3px;box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08)),0 0 0 3px var(--vi-button-focus-ring-glow, var(--vi-color-
|
|
6
|
+
const buttonStyles = "@charset \"UTF-8\";@layer reset,components,utilities;@layer components{.button{display:inline-flex;align-items:center;justify-content:center;gap:var(--vi-spacing-xs, 8px);border:var(--vi-border-width-thin, 1px) solid transparent;border-radius:var(--vi-button-shape-border-radius, var(--vi-border-radius-md, 4px));padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px)) var(--vi-button-spacing-padding-inline, var(--vi-spacing-md, 24px));font-size:var(--vi-button-typography-font-size, var(--vi-font-size-base, 16px));font-weight:var(--vi-button-typography-font-weight, var(--vi-font-weight-semibold, 600));line-height:var(--vi-line-height-tight, 1.2);cursor:pointer;-webkit-user-select:none;user-select:none;transition:opacity var(--vi-button-effect-transition-duration, .16s) ease,box-shadow var(--vi-button-effect-transition-duration, .16s) ease,transform var(--vi-button-effect-transition-duration, .16s) ease}}:host{display:inline-block}:host([size=xs]){--vi-button-spacing-padding-block: 2px;--vi-button-spacing-padding-inline: 8px;--vi-button-typography-font-size: var(--vi-font-size-xs, 12px)}:host([size=sm]){--vi-button-spacing-padding-block: 4px;--vi-button-spacing-padding-inline: 12px;--vi-button-typography-font-size: var(--vi-font-size-sm, 14px)}:host([size=lg]){--vi-button-spacing-padding-block: 12px;--vi-button-spacing-padding-inline: 24px;--vi-button-typography-font-size: var(--vi-font-size-lg, 18px)}:host([full-width]){display:block}:host([full-width]) .button{width:100%}:host([icon-only]) .button{aspect-ratio:1/1;padding:var(--vi-button-spacing-padding-block, var(--vi-spacing-sm, 16px));justify-content:center}:host([icon-only]) .label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host([variant=primary]) .button{background-color:var(--vi-button-surface-primary-background-color, var(--vi-color-primary, #3676d0));color:var(--vi-button-surface-primary-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=secondary]) .button{background-color:var(--vi-button-surface-secondary-background-color, var(--vi-color-secondary, #f0f4f8));color:var(--vi-button-surface-secondary-text-color, var(--vi-color-foreground, #111827));border-color:var(--vi-color-border, #e5e7eb)}:host([variant=danger]) .button{background-color:var(--vi-button-surface-danger-background-color, var(--vi-color-error, #ef4444));color:var(--vi-button-surface-danger-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=success]) .button{background-color:var(--vi-button-surface-success-background-color, var(--vi-color-success, #489167));color:var(--vi-button-surface-success-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=info]) .button{background-color:var(--vi-button-surface-info-background-color, var(--vi-color-info, #3676d0));color:var(--vi-button-surface-info-text-color, var(--vi-color-grey-100, #f5f5f5))}:host([variant=ghost]) .button{background-color:var(--vi-button-surface-ghost-background-color, transparent);color:var(--vi-button-surface-ghost-text-color, var(--vi-color-primary, #3676d0))}:host([disabled]) .button,.button:disabled{opacity:.6;cursor:not-allowed}.button:not(:disabled){box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08))}.button:hover:not(:disabled){opacity:.92;box-shadow:var(--vi-button-effect-shadow-hover, inset 0 1px 0 rgba(255, 255, 255, .18), 0 4px 8px rgba(0, 0, 0, .22), 0 2px 4px rgba(0, 0, 0, .1));transform:translateY(-1px)}.button:active:not(:disabled){box-shadow:var(--vi-button-effect-shadow-pressed, inset 0 2px 4px rgba(0, 0, 0, .22), 0 1px 1px rgba(0, 0, 0, .06));transform:translateY(1px);opacity:1}:host([variant=ghost]) .button:hover:not(:disabled){background-color:#0000000a;text-decoration:underline;opacity:1}:host([variant=ghost]) .button:not(:disabled){box-shadow:none}:host([variant=ghost]) .button:active:not(:disabled){box-shadow:inset 0 1px 3px #00000024;transform:translateY(0)}.button:focus-visible:not(:disabled){outline:var(--vi-border-width-base, 2px) solid var(--vi-button-focus-ring-color, var(--vi-color-primary, #3676d0));outline-offset:3px;box-shadow:var(--vi-button-effect-shadow-raised, inset 0 1px 0 rgba(255, 255, 255, .14), 0 2px 4px rgba(0, 0, 0, .18), 0 1px 2px rgba(0, 0, 0, .08)),0 0 0 3px var(--vi-button-focus-ring-glow, var(--vi-color-primary, #3676d0))}@media(prefers-reduced-motion:reduce){.button{transform:none!important;transition-property:color,background-color,border-color,box-shadow,opacity!important}}.icon{order:-1;display:inline-flex;flex-shrink:0}.icon[hidden]{display:none}:host([icon-placement=end]) .icon{order:1}::slotted(vi-icon),::slotted(svg){--vi-icon-size: 1em;width:1em;height:1em}.label{flex:1 1 auto;min-width:0}";
|
|
7
7
|
|
|
8
8
|
function applyDecs2203RFactory() {
|
|
9
9
|
function createAddInitializerMethod(initializers, decoratorFinishedRef) {
|
package/index.d.ts
CHANGED
|
@@ -4,8 +4,14 @@ export type { ControlStatus } from './base/validity-mixin.js';
|
|
|
4
4
|
export { ViButton } from './button/vi-button.js';
|
|
5
5
|
export type { ButtonVariant, ButtonSize, ButtonIconPlacement } from './button/vi-button.js';
|
|
6
6
|
export { ViInput } from './input/vi-input.js';
|
|
7
|
-
export type { InputType } from './input/vi-input.js';
|
|
7
|
+
export type { InputType, InputSize } 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 type { RadioSize } from './radio/vi-radio.js';
|
|
13
|
+
export { ViRadioGroup } from './radio/vi-radio-group.js';
|
|
14
|
+
export type { RadioGroupOrientation } from './radio/vi-radio-group.js';
|
|
15
|
+
export { ViCheckbox } from './checkbox/vi-checkbox.js';
|
|
16
|
+
export type { CheckboxSize } from './checkbox/vi-checkbox.js';
|
|
11
17
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -3,3 +3,6 @@ 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';
|
|
8
|
+
export { ViCheckbox } from './checkbox/vi-checkbox.js';
|
package/input/index.d.ts
CHANGED
package/input/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/input/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
|
package/input/vi-input.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ViElement } from '../base/vi-element.js';
|
|
|
8
8
|
* separate components.
|
|
9
9
|
*/
|
|
10
10
|
export type InputType = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url' | 'number';
|
|
11
|
+
export type InputSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
11
12
|
declare const ViInput_base: typeof ViElement & (new (...args: any[]) => import("../base/focusable-mixin.js").FocusableInterface) & (new (...args: any[]) => import("../base/validity-mixin.js").ValidityInterface);
|
|
12
13
|
/**
|
|
13
14
|
* vi-input
|
|
@@ -66,6 +67,8 @@ export declare class ViInput extends ViInput_base {
|
|
|
66
67
|
accessor value: string;
|
|
67
68
|
/** When true, disables the input and removes it from the tab order. */
|
|
68
69
|
accessor disabled: boolean;
|
|
70
|
+
/** Size scale — controls padding and font-size. */
|
|
71
|
+
accessor size: InputSize;
|
|
69
72
|
/** When true, the value cannot be edited but is still submitted. */
|
|
70
73
|
accessor readonly: boolean;
|
|
71
74
|
protected _testValidity(): Partial<ValidityStateFlags>;
|
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,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,mDAAmD;IACR,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAQ;IAE3E,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
|
*
|
|
@@ -280,7 +66,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
|
|
280
66
|
* - The binding is to a custom element property — use .prop=${val} instead
|
|
281
67
|
*/ const ifNonEmpty = (value)=>ifDefined(value === '' ? undefined : value ?? undefined);
|
|
282
68
|
|
|
283
|
-
const inputStyles = "@charset \"UTF-8\";@layer reset,components,utilities;@layer components{.input-field{display:flex;flex-direction:column;gap:var(--vi-input-spacing-field-gap, var(--vi-spacing-xs, 8px));width:100%}.input-control{appearance:none;-webkit-appearance:none;display:block;width:100%;box-sizing:border-box;min-height:var(--vi-input-sizing-min-height, 40px);border:var(--vi-border-width-thin, 1px) solid var(--vi-input-border-color, var(--vi-color-grey-300, #e0e0e0));border-radius:var(--vi-input-shape-border-radius, var(--vi-border-radius-lg, 8px));padding:var(--vi-input-spacing-padding-block, var(--vi-spacing-xs, 8px)) var(--vi-input-spacing-padding-inline, var(--vi-spacing-sm, 16px));font-family:var(--vi-font-family-base, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);font-size:var(--vi-input-typography-font-size, var(--vi-font-size-base, 16px));font-weight:var(--vi-font-weight-normal, 400);line-height:var(--vi-input-typography-line-height, var(--vi-line-height-normal, 1.5));color:var(--vi-input-text-color, var(--vi-color-foreground, #111827));background-color:var(--vi-input-background-color, var(--vi-color-background, #ffffff));transition:border-color .15s ease,box-shadow .15s ease}.input-control:hover:not(:focus-visible){border-color:var(--vi-input-border-color-hover, var(--vi-color-grey-500, #9e9e9e))}.input-control:focus-visible,.input-control:focus{outline:var(--vi-border-width-base, 2px) solid var(--vi-input-focus-ring-color, var(--vi-color-primary, #3676d0));outline-offset:0;box-shadow:0 0 0 3px var(--vi-input-focus-ring-glow, var(--vi-color-blue-200, #cee6ff))}.input-control::placeholder{color:var(--vi-input-placeholder-color, var(--vi-color-grey-500, #9e9e9e))}.input-helper{font-size:var(--vi-input-helper-size, var(--vi-font-size-xs, 12px));line-height:var(--vi-input-helper-leading, var(--vi-line-height-normal, 1.5));color:var(--vi-input-helper-color, var(--vi-color-grey-500, #9e9e9e))}.input-error{font-size:var(--vi-input-error-size, var(--vi-font-size-xs, 12px));line-height:var(--vi-input-error-leading, var(--vi-line-height-normal, 1.5));color:var(--vi-input-error-color, var(--vi-color-error, #ef4444))}@media(prefers-reduced-motion:reduce){.input-control{transition:none}}}:host{display:block;outline:none}:host([disabled]){opacity:.6;cursor:not-allowed;pointer-events:none}:host([status=invalid]){--vi-input-border-color: var(--vi-color-error, #ef4444);--vi-input-border-color-hover: var(--vi-color-
|
|
69
|
+
const inputStyles = "@charset \"UTF-8\";@layer reset,components,utilities;@layer components{.input-field{display:flex;flex-direction:column;gap:var(--vi-input-spacing-field-gap, var(--vi-spacing-xs, 8px));width:100%}.input-control{appearance:none;-webkit-appearance:none;display:block;width:100%;box-sizing:border-box;min-height:var(--vi-input-sizing-min-height, 40px);border:var(--vi-border-width-thin, 1px) solid var(--vi-input-border-color, var(--vi-color-grey-300, #e0e0e0));border-radius:var(--vi-input-shape-border-radius, var(--vi-border-radius-lg, 8px));padding:var(--vi-input-spacing-padding-block, var(--vi-spacing-xs, 8px)) var(--vi-input-spacing-padding-inline, var(--vi-spacing-sm, 16px));font-family:var(--vi-font-family-base, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);font-size:var(--vi-input-typography-font-size, var(--vi-font-size-base, 16px));font-weight:var(--vi-font-weight-normal, 400);line-height:var(--vi-input-typography-line-height, var(--vi-line-height-normal, 1.5));color:var(--vi-input-text-color, var(--vi-color-foreground, #111827));background-color:var(--vi-input-background-color, var(--vi-color-background, #ffffff));transition:border-color .15s ease,box-shadow .15s ease}.input-control:hover:not(:focus-visible){border-color:var(--vi-input-border-color-hover, var(--vi-color-grey-500, #9e9e9e))}.input-control:focus-visible,.input-control:focus{outline:var(--vi-border-width-base, 2px) solid var(--vi-input-focus-ring-color, var(--vi-color-primary, #3676d0));outline-offset:0;box-shadow:0 0 0 3px var(--vi-input-focus-ring-glow, var(--vi-color-blue-200, #cee6ff))}.input-control::placeholder{color:var(--vi-input-placeholder-color, var(--vi-color-grey-500, #9e9e9e))}.input-helper{font-size:var(--vi-input-helper-size, var(--vi-font-size-xs, 12px));line-height:var(--vi-input-helper-leading, var(--vi-line-height-normal, 1.5));color:var(--vi-input-helper-color, var(--vi-color-grey-500, #9e9e9e))}.input-error{font-size:var(--vi-input-error-size, var(--vi-font-size-xs, 12px));line-height:var(--vi-input-error-leading, var(--vi-line-height-normal, 1.5));color:var(--vi-input-error-color, var(--vi-color-error, #ef4444))}@media(prefers-reduced-motion:reduce){.input-control{transition:none}}}:host{display:block;outline:none}:host([disabled]){opacity:.6;cursor:not-allowed;pointer-events:none}:host([status=invalid]){--vi-input-border-color: var(--vi-color-error, #ef4444);--vi-input-border-color-hover: var(--vi-color-error, #ef4444);--vi-input-focus-ring-color: var(--vi-color-error, #ef4444);--vi-input-focus-ring-glow: var(--vi-color-red-100, #ffccce);--vi-input-label-color: var(--vi-color-error, #ef4444)}:host([status=valid]){--vi-input-border-color: var(--vi-color-success, #489167);--vi-input-border-color-hover: var(--vi-color-success, #489167);--vi-input-focus-ring-color: var(--vi-color-success, #489167);--vi-input-focus-ring-glow: var(--vi-color-green-100, #e6f0eb)}.input-validation{font-family:var(--vi-font-family-base, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);font-size:var(--vi-font-size-xs, 12px);line-height:var(--vi-line-height-normal, 1.5);letter-spacing:var(--vi-letter-spacing-wider, .05em);color:var(--vi-input-helper-color, var(--vi-color-grey-500, #9e9e9e))}.input-validation--invalid{color:var(--vi-input-error-color, var(--vi-color-error, #ef4444))}.input-validation--valid{color:var(--vi-input-success-color, var(--vi-color-success, #489167))}::slotted([slot=helper]){font-family:var(--vi-font-family-base, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);font-size:var(--vi-font-size-xs, 12px);line-height:var(--vi-line-height-normal, 1.5);letter-spacing:var(--vi-letter-spacing-wider, .05em);color:var(--vi-input-helper-color, var(--vi-color-grey-500, #9e9e9e))}:host([size=xs]){--vi-input-spacing-padding-block: 2px;--vi-input-spacing-padding-inline: 8px;--vi-input-typography-font-size: var(--vi-font-size-xs, 12px)}:host([size=sm]){--vi-input-spacing-padding-block: 4px;--vi-input-spacing-padding-inline: 12px;--vi-input-typography-font-size: var(--vi-font-size-sm, 14px)}:host([size=lg]){--vi-input-spacing-padding-block: 12px;--vi-input-spacing-padding-inline: 20px;--vi-input-typography-font-size: var(--vi-font-size-lg, 18px)}";
|
|
284
70
|
|
|
285
71
|
function applyDecs2203RFactory() {
|
|
286
72
|
function createAddInitializerMethod(initializers, decoratorFinishedRef) {
|
|
@@ -657,9 +443,9 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
|
|
|
657
443
|
function _identity(x) {
|
|
658
444
|
return x;
|
|
659
445
|
}
|
|
660
|
-
var _dec, _initClass, _ValidityMixin, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _dec8, _dec9, // ── ValidityMixin contract — must be declared as @property —————————————
|
|
446
|
+
var _dec, _initClass, _ValidityMixin, _dec1, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _dec8, _dec9, _dec10, // ── ValidityMixin contract — must be declared as @property —————————————
|
|
661
447
|
_init_status, _init_required, _init_validityMessage, // ── Public API ─────────────────────────────────────────────────────────────
|
|
662
|
-
/** Input type. Controls the keyboard/picker on mobile and browser validation hints. */ _init_type, /** Native input placeholder text. */ _init_placeholder, /** Form field name. Submitted with the form when set. */ _init_name, /** Current value. Synced to ElementInternals for form participation. */ _init_value, /** When true, disables the input and removes it from the tab order. */ _init_disabled, /** When true, the value cannot be edited but is still submitted. */ _init_readonly, _initProto;
|
|
448
|
+
/** Input type. Controls the keyboard/picker on mobile and browser validation hints. */ _init_type, /** Native input placeholder text. */ _init_placeholder, /** Form field name. Submitted with the form when set. */ _init_name, /** Current value. Synced to ElementInternals for form participation. */ _init_value, /** When true, disables the input and removes it from the tab order. */ _init_disabled, /** Size scale — controls padding and font-size. */ _init_size, /** When true, the value cannot be edited but is still submitted. */ _init_readonly, _initProto;
|
|
663
449
|
let _ViInput;
|
|
664
450
|
_dec = customElement('vi-input'), _dec1 = property({
|
|
665
451
|
reflect: true
|
|
@@ -673,6 +459,9 @@ _dec = customElement('vi-input'), _dec1 = property({
|
|
|
673
459
|
type: Boolean,
|
|
674
460
|
reflect: true
|
|
675
461
|
}), _dec9 = property({
|
|
462
|
+
type: String,
|
|
463
|
+
reflect: true
|
|
464
|
+
}), _dec10 = property({
|
|
676
465
|
type: Boolean,
|
|
677
466
|
reflect: true
|
|
678
467
|
});
|
|
@@ -683,7 +472,7 @@ new class extends _identity {
|
|
|
683
472
|
static{
|
|
684
473
|
class ViInput extends (_ValidityMixin = ValidityMixin(FocusableMixin(ViElement))) {
|
|
685
474
|
static{
|
|
686
|
-
({ e: [_init_status, _init_required, _init_validityMessage, _init_type, _init_placeholder, _init_name, _init_value, _init_disabled, _init_readonly, _initProto], c: [_ViInput, _initClass] } = _apply_decs_2203_r(this, [
|
|
475
|
+
({ e: [_init_status, _init_required, _init_validityMessage, _init_type, _init_placeholder, _init_name, _init_value, _init_disabled, _init_size, _init_readonly, _initProto], c: [_ViInput, _initClass] } = _apply_decs_2203_r(this, [
|
|
687
476
|
[
|
|
688
477
|
_dec1,
|
|
689
478
|
1,
|
|
@@ -727,6 +516,11 @@ new class extends _identity {
|
|
|
727
516
|
[
|
|
728
517
|
_dec9,
|
|
729
518
|
1,
|
|
519
|
+
"size"
|
|
520
|
+
],
|
|
521
|
+
[
|
|
522
|
+
_dec10,
|
|
523
|
+
1,
|
|
730
524
|
"readonly"
|
|
731
525
|
]
|
|
732
526
|
], [
|
|
@@ -797,12 +591,19 @@ new class extends _identity {
|
|
|
797
591
|
set disabled(_v) {
|
|
798
592
|
this.#___private_disabled_8 = _v;
|
|
799
593
|
}
|
|
800
|
-
#
|
|
594
|
+
#___private_size_9 = _init_size(this, 'md');
|
|
595
|
+
get size() {
|
|
596
|
+
return this.#___private_size_9;
|
|
597
|
+
}
|
|
598
|
+
set size(_v) {
|
|
599
|
+
this.#___private_size_9 = _v;
|
|
600
|
+
}
|
|
601
|
+
#___private_readonly_10 = _init_readonly(this, false);
|
|
801
602
|
get readonly() {
|
|
802
|
-
return this.#
|
|
603
|
+
return this.#___private_readonly_10;
|
|
803
604
|
}
|
|
804
605
|
set readonly(_v) {
|
|
805
|
-
this.#
|
|
606
|
+
this.#___private_readonly_10 = _v;
|
|
806
607
|
}
|
|
807
608
|
// ── ValidityMixin hook ─────────────────────────────────────────────────────
|
|
808
609
|
// _testValidity is declared protected in ValidityInterface, but TypeScript's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vialiq/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.3.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 };
|