@vialiq/web-components 0.1.2 → 0.2.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/base/focus-trap-mixin.d.ts +121 -0
- package/base/focus-trap-mixin.d.ts.map +1 -0
- package/base/focusable-mixin.d.ts +82 -0
- package/base/focusable-mixin.d.ts.map +1 -0
- package/base/focusable-selector.d.ts +80 -0
- package/base/focusable-selector.d.ts.map +1 -0
- package/base/if-non-empty.d.ts +62 -0
- package/base/if-non-empty.d.ts.map +1 -0
- package/base/validity-mixin.d.ts +231 -0
- package/base/validity-mixin.d.ts.map +1 -0
- package/base/vi-element.d.ts +12 -0
- package/base/vi-element.d.ts.map +1 -0
- package/button/index.d.ts +2 -0
- package/button/index.d.ts.map +1 -0
- package/button/vi-button.d.ts +68 -0
- package/button/vi-button.d.ts.map +1 -0
- package/button/vi-button.js +3 -152
- package/focusable-mixin-CmxOyPX5.js +155 -0
- package/icons/registry.d.ts +29 -0
- package/icons/registry.d.ts.map +1 -0
- package/icons/vi-icon.d.ts +47 -0
- package/icons/vi-icon.d.ts.map +1 -0
- package/index.d.ts +11 -0
- package/index.js +1 -0
- package/input/index.d.ts +3 -0
- package/input/index.d.ts.map +1 -0
- package/input/vi-input.d.ts +89 -0
- package/input/vi-input.d.ts.map +1 -0
- package/input/vi-input.js +937 -0
- package/package.json +8 -3
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
type Constructor<T = object> = new (...args: any[]) => T;
|
|
3
|
+
/**
|
|
4
|
+
* Type-only declaration of the shape FocusTrapMixin adds to a class.
|
|
5
|
+
* `declare class` emits no runtime code — it is a TS-only contract.
|
|
6
|
+
*/
|
|
7
|
+
export declare class FocusTrapInterface {
|
|
8
|
+
/**
|
|
9
|
+
* Activates the focus trap.
|
|
10
|
+
* Queries all focusable elements in the shadow root (and slotted content),
|
|
11
|
+
* focuses the first one (or a provided initial target), and begins
|
|
12
|
+
* intercepting Tab / Shift+Tab to cycle focus within the component.
|
|
13
|
+
*
|
|
14
|
+
* @param initialFocus - Optional element to focus first. Defaults to the
|
|
15
|
+
* first focusable element found in the shadow root.
|
|
16
|
+
*/
|
|
17
|
+
protected _activateFocusTrap(initialFocus?: HTMLElement): void;
|
|
18
|
+
/**
|
|
19
|
+
* Deactivates the focus trap.
|
|
20
|
+
* Removes the Tab intercept and restores focus to the element that was
|
|
21
|
+
* focused immediately before `_activateFocusTrap()` was called.
|
|
22
|
+
*/
|
|
23
|
+
protected _deactivateFocusTrap(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* FocusTrapMixin
|
|
27
|
+
*
|
|
28
|
+
* Traps keyboard focus within a component's shadow root.
|
|
29
|
+
* Intended for overlay components: vi-modal, vi-dialog, vi-drawer.
|
|
30
|
+
*
|
|
31
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
32
|
+
* HOW IT WORKS
|
|
33
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* activate → snapshot pre-trap focus → focus initial element → listen Tab
|
|
36
|
+
* Tab → if on last focusable → wrap to first
|
|
37
|
+
* Shift+Tab → if on first focusable → wrap to last
|
|
38
|
+
* deactivate→ remove listener → restore pre-trap focus
|
|
39
|
+
*
|
|
40
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
* CRITICAL: shadowRoot.activeElement vs document.activeElement
|
|
42
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* Inside a shadow root, `document.activeElement` returns the HOST element
|
|
45
|
+
* (e.g. the `<vi-modal>` tag), NOT the inner focused element.
|
|
46
|
+
* To correctly identify which inner element is focused, ALWAYS use:
|
|
47
|
+
*
|
|
48
|
+
* this.shadowRoot!.activeElement
|
|
49
|
+
*
|
|
50
|
+
* This is the deepest focused element within the shadow boundary.
|
|
51
|
+
* For slotted content (light DOM children), `shadowRoot.activeElement`
|
|
52
|
+
* returns the `<slot>` element, not the focused child — in that case,
|
|
53
|
+
* use `document.activeElement` to get the actual slotted focused element.
|
|
54
|
+
*
|
|
55
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
56
|
+
* FOCUSABLE ELEMENT COLLECTION
|
|
57
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
58
|
+
*
|
|
59
|
+
* Two sources are queried and merged:
|
|
60
|
+
*
|
|
61
|
+
* 1. Shadow DOM: shadowRoot.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
62
|
+
* Covers native elements (<button>, <input>, etc.) and vi-* host elements
|
|
63
|
+
* that use FocusableMixin (vi-button, vi-input, etc.).
|
|
64
|
+
*
|
|
65
|
+
* 2. Slotted content: each <slot>.assignedElements({ flatten: true })
|
|
66
|
+
* filtered by FOCUSABLE_SELECTOR.
|
|
67
|
+
* Covers focusable light-DOM children slotted into the component.
|
|
68
|
+
* Example: a <vi-button> slotted into <vi-modal>'s footer slot.
|
|
69
|
+
*
|
|
70
|
+
* Both lists are combined and deduplicated into a single ordered array.
|
|
71
|
+
* DOM order is preserved (querySelector returns elements in tree order).
|
|
72
|
+
*
|
|
73
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
74
|
+
* USAGE
|
|
75
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
76
|
+
*
|
|
77
|
+
* class ViModal extends FocusTrapMixin(ViElement) {
|
|
78
|
+
* @property({ type: Boolean, reflect: true }) accessor open = false;
|
|
79
|
+
*
|
|
80
|
+
* override updated(changed: PropertyValues): void {
|
|
81
|
+
* super.updated(changed);
|
|
82
|
+
* if (changed.has('open')) {
|
|
83
|
+
* if (this.open) {
|
|
84
|
+
* // Optional: pass a specific element to focus first.
|
|
85
|
+
* // If omitted, the first focusable in the shadow root is used.
|
|
86
|
+
* this._activateFocusTrap();
|
|
87
|
+
* } else {
|
|
88
|
+
* this._deactivateFocusTrap();
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
* }
|
|
93
|
+
*
|
|
94
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
95
|
+
* ACCESSIBILITY CONTRACT
|
|
96
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
97
|
+
*
|
|
98
|
+
* - The trapping component MUST have role="dialog" (or "alertdialog") and
|
|
99
|
+
* aria-modal="true" so screen readers also confine their virtual cursor.
|
|
100
|
+
* - Escape key handling is NOT part of this mixin — the component owns that
|
|
101
|
+
* (it is component-specific: vi-modal may close on Escape, vi-drawer may not).
|
|
102
|
+
* - The trap does NOT prevent focus from moving to the browser chrome (URL bar,
|
|
103
|
+
* tab bar) — that is intentional and required by WCAG 2.1 SC 2.1.2.
|
|
104
|
+
*
|
|
105
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
106
|
+
* COMPOSITION ORDER
|
|
107
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
108
|
+
*
|
|
109
|
+
* FocusTrapMixin does NOT require FocusableMixin. Most overlay components
|
|
110
|
+
* (vi-modal, vi-drawer) are NOT themselves focusable tab stops — they are
|
|
111
|
+
* focus containers. Use independently:
|
|
112
|
+
*
|
|
113
|
+
* class ViModal extends FocusTrapMixin(ViElement) { ... }
|
|
114
|
+
*
|
|
115
|
+
* If you need both (unusual — a container that is also a tab stop):
|
|
116
|
+
*
|
|
117
|
+
* class ViPanel extends FocusTrapMixin(FocusableMixin(ViElement)) { ... }
|
|
118
|
+
*/
|
|
119
|
+
export declare function FocusTrapMixin<T extends Constructor<LitElement>>(Base: T): T & Constructor<FocusTrapInterface>;
|
|
120
|
+
export {};
|
|
121
|
+
//# sourceMappingURL=focus-trap-mixin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-trap-mixin.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/focus-trap-mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAKjC,KAAK,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC;;;;;;;;OAQG;IACH,SAAS,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,WAAW,GAAG,IAAI;IAE9D;;;;OAIG;IACH,SAAS,CAAC,oBAAoB,IAAI,IAAI;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6FG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EAC9D,IAAI,EAAE,CAAC,GACN,CAAC,GAAG,WAAW,CAAC,kBAAkB,CAAC,CA2KrC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
type Constructor<T = object> = new (...args: any[]) => T;
|
|
3
|
+
/**
|
|
4
|
+
* Type-only declaration of the shape FocusableMixin adds to a class.
|
|
5
|
+
* Using `declare class` emits no runtime code — it exists solely to give
|
|
6
|
+
* TypeScript a concrete type to work with when extending the mixin result.
|
|
7
|
+
* This avoids the "anonymous class private member" errors that occur when
|
|
8
|
+
* TypeScript tries to inline LitElement's private fields into the return type.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FocusableInterface {
|
|
11
|
+
protected get _focusableElement(): HTMLElement | null;
|
|
12
|
+
focus(options?: FocusOptions): void;
|
|
13
|
+
/**
|
|
14
|
+
* Enables or disables host focus participation.
|
|
15
|
+
* Call this whenever the component's `disabled` state changes so the
|
|
16
|
+
* tabIndex policy stays centralized in the mixin rather than scattered
|
|
17
|
+
* across component `updated()` hooks.
|
|
18
|
+
*
|
|
19
|
+
* override updated(changed: PropertyValues): void {
|
|
20
|
+
* super.updated(changed);
|
|
21
|
+
* if (changed.has('disabled')) this._setHostFocusable(!this.disabled);
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
protected _setHostFocusable(enabled: boolean): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* FocusableMixin
|
|
28
|
+
*
|
|
29
|
+
* Applies to all interactive Vi components. Provides:
|
|
30
|
+
*
|
|
31
|
+
* 1. `delegatesFocus: true` on the shadow root — when the host is Tab-focused
|
|
32
|
+
* or `.focus()` is called on it, the browser routes focus to the inner
|
|
33
|
+
* native control. Also activates `:focus` and `:focus-within` on the host.
|
|
34
|
+
*
|
|
35
|
+
* 2. A `focus()` override that delegates to `_focusableElement` so callers
|
|
36
|
+
* can do `myInput.focus()` and it Just Works without knowing shadow internals.
|
|
37
|
+
*
|
|
38
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
39
|
+
* ARCHITECTURE: host is the tab stop
|
|
40
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
*
|
|
42
|
+
* Host: tabIndex = 0 ← consumer-visible light-DOM tab stop
|
|
43
|
+
* Inner element: tabindex="0" ← participates in shadow root's own tab
|
|
44
|
+
* order so :focus-visible fires reliably
|
|
45
|
+
* delegatesFocus: true ← routes host focus → inner element
|
|
46
|
+
*
|
|
47
|
+
* This means:
|
|
48
|
+
* - Tab → lands on host → delegatesFocus → inner element gets visual focus
|
|
49
|
+
* - `:host(:focus)` and `:host(:focus-within)` both activate correctly
|
|
50
|
+
* - `:focus-visible` on the inner element fires reliably for all browsers
|
|
51
|
+
* (keyboard vs mouse distinction works without browser-specific hacks)
|
|
52
|
+
* - `element.focus()` calls our override → inner element focused explicitly
|
|
53
|
+
* - Consumer sets tabindex="-1" on host to remove from tab order entirely
|
|
54
|
+
* - Consumer sets tabindex="2" for explicit ordering — just works
|
|
55
|
+
*
|
|
56
|
+
* NOTE: tabindex="0" on the inner element does NOT create a second light-DOM
|
|
57
|
+
* tab stop. Shadow DOM children only participate in the shadow root's local
|
|
58
|
+
* tab order; the host remains the single entry point from the outer document.
|
|
59
|
+
* The difference from tabindex="-1" is that :focus-visible propagation through
|
|
60
|
+
* delegatesFocus is more consistent when the delegated target is a proper
|
|
61
|
+
* sequential-focus participant.
|
|
62
|
+
*
|
|
63
|
+
* DISABLED: When the `disabled` prop changes, the component MUST sync the
|
|
64
|
+
* host's tabIndex:
|
|
65
|
+
*
|
|
66
|
+
* override updated(changed: PropertyValues) {
|
|
67
|
+
* super.updated(changed);
|
|
68
|
+
* if (changed.has('disabled')) {
|
|
69
|
+
* this.tabIndex = this.disabled ? -1 : (previous tabIndex value or 0);
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* Usage:
|
|
74
|
+
* class ViInput extends FocusableMixin(ViElement) {
|
|
75
|
+
* protected override get _focusableElement() {
|
|
76
|
+
* return this.shadowRoot?.querySelector('input') ?? null;
|
|
77
|
+
* }
|
|
78
|
+
* }
|
|
79
|
+
*/
|
|
80
|
+
export declare function FocusableMixin<T extends Constructor<LitElement>>(Base: T): T & Constructor<FocusableInterface>;
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=focusable-mixin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusable-mixin.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/focusable-mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAIjC,KAAK,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,SAAS,KAAK,iBAAiB,IAAI,WAAW,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI;IACnC;;;;;;;;;;OAUG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EAC9D,IAAI,EAAE,CAAC,GACN,CAAC,GAAG,WAAW,CAAC,kBAAkB,CAAC,CA8GrC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FOCUSABLE_SELECTOR
|
|
3
|
+
*
|
|
4
|
+
* A CSS selector that matches all tabbable elements within a container.
|
|
5
|
+
*
|
|
6
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
7
|
+
* WHY CUSTOM ELEMENTS MUST BE LISTED EXPLICITLY
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* CSS cannot see through a shadow root. A generic `[tabindex]:not([tabindex="-1"])`
|
|
11
|
+
* will NOT match a `<vi-button>` even though vi-button has tabIndex=-1 on its
|
|
12
|
+
* host and tabIndex=0 on its inner <button>. The inner <button> is inside the
|
|
13
|
+
* shadow root and is invisible to an external querySelector.
|
|
14
|
+
*
|
|
15
|
+
* Each `vi-*` element listed here uses FocusableMixin, which means:
|
|
16
|
+
* - The host has tabIndex=-1 (not a sequential tab stop itself)
|
|
17
|
+
* - `delegatesFocus: true` routes clicks inward
|
|
18
|
+
* - Calling `.focus()` on the host delegates to the inner control
|
|
19
|
+
*
|
|
20
|
+
* So treating the host as the focusable unit is correct — the browser's
|
|
21
|
+
* tab sequence reaches the inner control via the host, and programmatic
|
|
22
|
+
* `.focus()` on the host works because of FocusableMixin.
|
|
23
|
+
*
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* MAINTENANCE RULE
|
|
26
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Update this file whenever a new focusable vi-* component ships.
|
|
29
|
+
* This is the SINGLE source of truth. FocusTrapMixin, any roving-tabindex
|
|
30
|
+
* manager, and a11y tests all import from here — do not duplicate this list.
|
|
31
|
+
*
|
|
32
|
+
* When adding a new component:
|
|
33
|
+
* 1. Add its tag to PHASE_1_VI_COMPONENTS or PHASE_2_VI_COMPONENTS below.
|
|
34
|
+
* 2. Uncomment it in the exported FOCUSABLE_SELECTOR when the component ships.
|
|
35
|
+
*
|
|
36
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
37
|
+
* USAGE
|
|
38
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
39
|
+
*
|
|
40
|
+
* import { FOCUSABLE_SELECTOR } from '../base/focusable-selector.js';
|
|
41
|
+
*
|
|
42
|
+
* // All focusables in a shadow root (native + vi-* hosts):
|
|
43
|
+
* const focusable = [
|
|
44
|
+
* ...this.shadowRoot!.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
|
|
45
|
+
* ];
|
|
46
|
+
*
|
|
47
|
+
* // Include slotted content too (for components that slot focusable children):
|
|
48
|
+
* this.shadowRoot!.querySelectorAll('slot').forEach((slotEl) => {
|
|
49
|
+
* (slotEl as HTMLSlotElement)
|
|
50
|
+
* .assignedElements({ flatten: true })
|
|
51
|
+
* .forEach((el) => {
|
|
52
|
+
* if (el.matches(FOCUSABLE_SELECTOR)) {
|
|
53
|
+
* focusable.push(el as HTMLElement);
|
|
54
|
+
* }
|
|
55
|
+
* });
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
59
|
+
* DISABLED EXCLUSION RULES
|
|
60
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
*
|
|
62
|
+
* Native elements: :not([disabled]) excludes them from the selector.
|
|
63
|
+
* vi-* elements: :not([disabled]) excludes the host — works because vi-*
|
|
64
|
+
* components reflect `disabled` as an attribute via @property({ reflect: true }).
|
|
65
|
+
*
|
|
66
|
+
* tabindex="-1" exclusion: intentionally NOT in this selector.
|
|
67
|
+
* Rationale: elements with tabindex="-1" are programmatically focusable
|
|
68
|
+
* but not in the sequential tab order. Whether to include them in a trap
|
|
69
|
+
* is a caller decision (FocusTrapMixin excludes them via :not([tabindex="-1"])).
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* A CSS selector matching all tabbable elements:
|
|
73
|
+
* - Native HTML controls (button, input, select, textarea, a, area)
|
|
74
|
+
* - Elements with an explicit non-negative tabindex
|
|
75
|
+
* - All focusable vi-* components (listed explicitly — see above)
|
|
76
|
+
*
|
|
77
|
+
* Disabled elements and elements with tabindex="-1" are excluded.
|
|
78
|
+
*/
|
|
79
|
+
export declare const FOCUSABLE_SELECTOR: string;
|
|
80
|
+
//# sourceMappingURL=focusable-selector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusable-selector.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/focusable-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAiCH;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAInB,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ifNonEmpty — conditional attribute directive
|
|
3
|
+
*
|
|
4
|
+
* A wrapper around Lit's `ifDefined` that additionally removes the attribute
|
|
5
|
+
* when the value is an empty string. Use this for every optional string
|
|
6
|
+
* attribute on inner native elements where `""` and "absent" have different
|
|
7
|
+
* meaning for browsers and screen readers.
|
|
8
|
+
*
|
|
9
|
+
* Problem with raw `ifDefined`:
|
|
10
|
+
* - `ifDefined(undefined)` → removes the attribute ✅
|
|
11
|
+
* - `ifDefined(null)` → removes the attribute ✅
|
|
12
|
+
* - `ifDefined('')` → sets attribute to "" ❌
|
|
13
|
+
*
|
|
14
|
+
* With `ifNonEmpty`:
|
|
15
|
+
* - `ifNonEmpty(undefined)` → removes the attribute ✅
|
|
16
|
+
* - `ifNonEmpty(null)` → removes the attribute ✅
|
|
17
|
+
* - `ifNonEmpty('')` → removes the attribute ✅
|
|
18
|
+
* - `ifNonEmpty('hello')` → sets attribute to "hello" ✅
|
|
19
|
+
*
|
|
20
|
+
* Why it matters — real screen reader / browser bugs caused by `=""`:
|
|
21
|
+
* - `placeholder=""` → JAWS/NVDA still announce it as an empty placeholder
|
|
22
|
+
* - `aria-label=""` → NVDA reads "blank" instead of deriving the name elsewhere
|
|
23
|
+
* - `aria-describedby=""`→ browsers may still look for id="" element
|
|
24
|
+
* - `title=""` → browsers show an empty tooltip on hover in some engines
|
|
25
|
+
*
|
|
26
|
+
* ---
|
|
27
|
+
*
|
|
28
|
+
* USAGE
|
|
29
|
+
*
|
|
30
|
+
* Import in any shadow template that has optional string attributes:
|
|
31
|
+
*
|
|
32
|
+
* import { ifNonEmpty } from '../base/if-non-empty.js';
|
|
33
|
+
*
|
|
34
|
+
* In the template:
|
|
35
|
+
*
|
|
36
|
+
* // ✅ Use ifNonEmpty for optional string attributes on inner native elements
|
|
37
|
+
* <input
|
|
38
|
+
* placeholder=${ifNonEmpty(this.placeholder)}
|
|
39
|
+
* aria-label=${ifNonEmpty(this.label)}
|
|
40
|
+
* aria-describedby=${ifNonEmpty(this._descriptionId)}
|
|
41
|
+
* />
|
|
42
|
+
*
|
|
43
|
+
* // ❌ Do NOT use for boolean attributes — Lit has ?attr=${bool} for that
|
|
44
|
+
* // ❌ Do NOT use for property bindings — Lit has .prop=${val} for that
|
|
45
|
+
* // ❌ Do NOT use for event bindings — Lit has @event=${handler} for that
|
|
46
|
+
*
|
|
47
|
+
* ---
|
|
48
|
+
*
|
|
49
|
+
* WHEN TO USE vs NOT USE
|
|
50
|
+
*
|
|
51
|
+
* Use ifNonEmpty when:
|
|
52
|
+
* - The attribute is optional (component has a prop that defaults to '')
|
|
53
|
+
* - The inner native element is a standard HTML element (input, button, a, etc.)
|
|
54
|
+
* - The attribute has accessibility or UI meaning when absent vs present
|
|
55
|
+
*
|
|
56
|
+
* Skip ifNonEmpty when:
|
|
57
|
+
* - The attribute is always required (e.g. type="button" is never absent)
|
|
58
|
+
* - You need the attribute to literally be "" (rare, document when intentional)
|
|
59
|
+
* - The binding is to a custom element property — use .prop=${val} instead
|
|
60
|
+
*/
|
|
61
|
+
export declare const ifNonEmpty: (value: string | null | undefined) => string | typeof import("lit-html").nothing;
|
|
62
|
+
//# sourceMappingURL=if-non-empty.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"if-non-empty.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/if-non-empty.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,GAAG,IAAI,GAAG,SAAS,+CACC,CAAC"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
type Constructor<T = object> = new (...args: any[]) => T;
|
|
3
|
+
/**
|
|
4
|
+
* Tri-state visual status for form controls.
|
|
5
|
+
*
|
|
6
|
+
* - `'default'` — no validation styling (neutral / untouched)
|
|
7
|
+
* - `'invalid'` — red border, error colours; set by the form when validation fails
|
|
8
|
+
* - `'valid'` — green border, success colours; set explicitly by the parent when
|
|
9
|
+
* it wants to confirm a correct value (independent of message)
|
|
10
|
+
*
|
|
11
|
+
* Designed to be driven from outside (Angular binding, React prop, plain JS) so
|
|
12
|
+
* the component never decides on its own that it is "valid" — that is always an
|
|
13
|
+
* explicit, intentional signal from the consuming code.
|
|
14
|
+
*/
|
|
15
|
+
export type ControlStatus = 'default' | 'valid' | 'invalid';
|
|
16
|
+
/**
|
|
17
|
+
* Type-only declaration of the shape ValidityMixin adds to a class.
|
|
18
|
+
* `declare class` emits no runtime code — it is a TS-only contract.
|
|
19
|
+
*
|
|
20
|
+
* Properties listed here (invalid, required, validityMessage, value) must be
|
|
21
|
+
* declared as reactive `@property` accessors on the subclass. The mixin reads
|
|
22
|
+
* and writes them but cannot create them — they must exist on the concrete class
|
|
23
|
+
* for Lit's reactivity system to pick them up.
|
|
24
|
+
*
|
|
25
|
+
* _internals must be set via `this.attachInternals()` on the subclass.
|
|
26
|
+
* The class must also declare `static formAssociated = true`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ValidityInterface {
|
|
29
|
+
/**
|
|
30
|
+
* Visual/validation state of the control. Reflects as the `[status]` attribute.
|
|
31
|
+
* - `'default'` — no validation styling
|
|
32
|
+
* - `'invalid'` — red border / error colours
|
|
33
|
+
* - `'valid'` — green border / success colours
|
|
34
|
+
*
|
|
35
|
+
* Set by the mixin's constraint-validation methods AND by the consuming
|
|
36
|
+
* code/framework. The parent always controls this — the component never
|
|
37
|
+
* auto-promotes itself to `'valid'`.
|
|
38
|
+
*/
|
|
39
|
+
get status(): ControlStatus;
|
|
40
|
+
set status(value: ControlStatus);
|
|
41
|
+
/** Whether a value is required. Drives the valueMissing validity flag. */
|
|
42
|
+
get required(): boolean;
|
|
43
|
+
set required(value: boolean);
|
|
44
|
+
/** The human-readable validation message displayed in the component UI. */
|
|
45
|
+
get validityMessage(): string;
|
|
46
|
+
set validityMessage(value: string);
|
|
47
|
+
/** The current component value. Read by _testValidity for required check. */
|
|
48
|
+
get value(): string;
|
|
49
|
+
set value(v: string);
|
|
50
|
+
/**
|
|
51
|
+
* ElementInternals instance. Subclass MUST declare:
|
|
52
|
+
* protected readonly _internals = this.attachInternals();
|
|
53
|
+
* and set:
|
|
54
|
+
* static formAssociated = true;
|
|
55
|
+
*/
|
|
56
|
+
protected readonly _internals: ElementInternals;
|
|
57
|
+
/**
|
|
58
|
+
* Checks if the current value satisfies all constraints.
|
|
59
|
+
* Fires a cancelable 'invalid' event (bubbles: false) if invalid.
|
|
60
|
+
* Does NOT show browser validation tooltip.
|
|
61
|
+
* Returns true if valid.
|
|
62
|
+
*/
|
|
63
|
+
checkValidity(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Checks validity AND triggers the browser's built-in validation UI
|
|
66
|
+
* (tooltip near the field). Delegates to ElementInternals.reportValidity().
|
|
67
|
+
* Returns true if valid.
|
|
68
|
+
*/
|
|
69
|
+
reportValidity(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Sets an arbitrary custom validation message.
|
|
72
|
+
* Pass an empty string to clear the custom error and restore validity.
|
|
73
|
+
* Syncs immediately to ElementInternals so native form constraint API works.
|
|
74
|
+
*/
|
|
75
|
+
setCustomValidity(message: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Returns the set of ValidityStateFlags describing why the value is invalid.
|
|
78
|
+
* Return {} (empty object) when the value is valid.
|
|
79
|
+
*
|
|
80
|
+
* The base implementation returns {} (always valid).
|
|
81
|
+
* Subclasses override this to add component-specific checks.
|
|
82
|
+
*
|
|
83
|
+
* Standard flags (all optional, all boolean):
|
|
84
|
+
* valueMissing — required field has no value
|
|
85
|
+
* tooShort — value is shorter than minlength
|
|
86
|
+
* tooLong — value exceeds maxlength
|
|
87
|
+
* rangeUnderflow — numeric value < min
|
|
88
|
+
* rangeOverflow — numeric value > max
|
|
89
|
+
* patternMismatch— value doesn't match pattern
|
|
90
|
+
* typeMismatch — value not well-formed for type (email, url, etc.)
|
|
91
|
+
* badInput — user input cannot be converted to a value at all
|
|
92
|
+
* customError — setCustomValidity() was called with a non-empty string
|
|
93
|
+
* stepMismatch — value doesn't conform to step
|
|
94
|
+
*/
|
|
95
|
+
protected _testValidity(): Partial<ValidityStateFlags>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* ValidityMixin
|
|
99
|
+
*
|
|
100
|
+
* Adds the standard form validation API (`checkValidity`, `reportValidity`,
|
|
101
|
+
* `setCustomValidity`) to any form-associated Lit element.
|
|
102
|
+
*
|
|
103
|
+
* Backed by the native `ElementInternals` API so the component participates
|
|
104
|
+
* in `HTMLFormElement` constraint validation, `.elements`, and browser
|
|
105
|
+
* validation UI — exactly like a native `<input>`.
|
|
106
|
+
*
|
|
107
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
108
|
+
* MINIMUM SUBCLASS REQUIREMENTS
|
|
109
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
110
|
+
*
|
|
111
|
+
* 1. Declare static formAssociated = true;
|
|
112
|
+
* Makes the browser register this element as a form participant.
|
|
113
|
+
*
|
|
114
|
+
* 2. Attach ElementInternals:
|
|
115
|
+
* protected readonly _internals = this.attachInternals();
|
|
116
|
+
* Must be a field initializer (runs after super() in the constructor).
|
|
117
|
+
*
|
|
118
|
+
* 3. Declare reactive properties (MUST be @property so Lit tracks changes):
|
|
119
|
+
* @property({ reflect: true }) accessor status: ControlStatus = 'default';
|
|
120
|
+
* @property({ type: Boolean, reflect: true }) accessor required = false;
|
|
121
|
+
* @property() accessor validityMessage = '';
|
|
122
|
+
* @property() accessor value = '';
|
|
123
|
+
*
|
|
124
|
+
* 4. Sync value to internals on every value change:
|
|
125
|
+
* override updated(changed: PropertyValues): void {
|
|
126
|
+
* super.updated(changed);
|
|
127
|
+
* if (changed.has('value')) {
|
|
128
|
+
* this._internals.setFormValue(this.value);
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* 5. Handle form reset:
|
|
133
|
+
* formResetCallback(): void {
|
|
134
|
+
* this.value = this.getAttribute('value') ?? '';
|
|
135
|
+
* this.status = 'default';
|
|
136
|
+
* this.validityMessage = '';
|
|
137
|
+
* }
|
|
138
|
+
*
|
|
139
|
+
* 6. Handle fieldset/form disable:
|
|
140
|
+
* formDisabledCallback(disabled: boolean): void {
|
|
141
|
+
* this.disabled = disabled;
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
145
|
+
* OVERRIDE _testValidity() FOR CUSTOM CONSTRAINTS
|
|
146
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
147
|
+
*
|
|
148
|
+
* protected override _testValidity(): Partial<ValidityStateFlags> {
|
|
149
|
+
* if (this.required && !this.value) return { valueMissing: true };
|
|
150
|
+
* return {};
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* Chain constraints for components with multiple rules (e.g. vi-input[type="number"]):
|
|
154
|
+
*
|
|
155
|
+
* protected override _testValidity(): Partial<ValidityStateFlags> {
|
|
156
|
+
* if (this.required && !this.value) return { valueMissing: true };
|
|
157
|
+
* if (this.minlength && this.value.length < this.minlength)
|
|
158
|
+
* return { tooShort: true };
|
|
159
|
+
* if (this.maxlength && this.value.length > this.maxlength)
|
|
160
|
+
* return { tooLong: true };
|
|
161
|
+
* return {};
|
|
162
|
+
* }
|
|
163
|
+
*
|
|
164
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
165
|
+
* FULL USAGE EXAMPLE — vi-input
|
|
166
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
167
|
+
*
|
|
168
|
+
* import { property } from 'lit/decorators.js';
|
|
169
|
+
* import { ValidityMixin } from '../base/validity-mixin.js';
|
|
170
|
+
* import { FocusableMixin } from '../base/focusable-mixin.js';
|
|
171
|
+
* import { ViElement } from '../base/vi-element.js';
|
|
172
|
+
*
|
|
173
|
+
* @customElement('vi-input')
|
|
174
|
+
* export class ViInput extends ValidityMixin(FocusableMixin(ViElement)) {
|
|
175
|
+
* static override formAssociated = true;
|
|
176
|
+
* protected readonly _internals = this.attachInternals();
|
|
177
|
+
*
|
|
178
|
+
* @property({ reflect: true }) accessor status: ControlStatus = 'default';
|
|
179
|
+
* @property({ type: Boolean, reflect: true }) accessor required = false;
|
|
180
|
+
* @property() accessor validityMessage = '';
|
|
181
|
+
* @property() accessor value = '';
|
|
182
|
+
*
|
|
183
|
+
* protected override _testValidity(): Partial<ValidityStateFlags> {
|
|
184
|
+
* if (this.required && !this.value) return { valueMissing: true };
|
|
185
|
+
* return {};
|
|
186
|
+
* }
|
|
187
|
+
*
|
|
188
|
+
* override updated(changed: PropertyValues): void {
|
|
189
|
+
* super.updated(changed);
|
|
190
|
+
* if (changed.has('value')) this._internals.setFormValue(this.value);
|
|
191
|
+
* }
|
|
192
|
+
*
|
|
193
|
+
* formResetCallback(): void {
|
|
194
|
+
* this.value = this.getAttribute('value') ?? '';
|
|
195
|
+
* this.status = 'default';
|
|
196
|
+
* this.validityMessage = '';
|
|
197
|
+
* }
|
|
198
|
+
*
|
|
199
|
+
* formDisabledCallback(disabled: boolean): void {
|
|
200
|
+
* this.disabled = disabled;
|
|
201
|
+
* }
|
|
202
|
+
* }
|
|
203
|
+
*
|
|
204
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
205
|
+
* INVALID EVENT BEHAVIOUR
|
|
206
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
207
|
+
*
|
|
208
|
+
* checkValidity() fires a cancelable 'invalid' event when validation fails.
|
|
209
|
+
* The event does NOT bubble (matches native form element behaviour).
|
|
210
|
+
* Consumer can suppress the default UI by calling event.preventDefault().
|
|
211
|
+
*
|
|
212
|
+
* Example:
|
|
213
|
+
* myInput.addEventListener('invalid', (e) => {
|
|
214
|
+
* e.preventDefault(); // suppress browser tooltip
|
|
215
|
+
* showMyCustomError(myInput.validityMessage);
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
219
|
+
* MIXIN COMPOSITION ORDER
|
|
220
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
221
|
+
*
|
|
222
|
+
* ValidityMixin should wrap FocusableMixin (outermost):
|
|
223
|
+
* ValidityMixin(FocusableMixin(ViElement))
|
|
224
|
+
*
|
|
225
|
+
* Reason: ValidityMixin only adds methods (checkValidity etc.). It does not
|
|
226
|
+
* touch shadowRootOptions or focus delegation, so order has no side-effects.
|
|
227
|
+
* Convention is: functionality mixins wrap infrastructure mixins.
|
|
228
|
+
*/
|
|
229
|
+
export declare function ValidityMixin<T extends Constructor<LitElement>>(Base: T): T & Constructor<ValidityInterface>;
|
|
230
|
+
export {};
|
|
231
|
+
//# sourceMappingURL=validity-mixin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validity-mixin.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/validity-mixin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAIjC,KAAK,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;IAOpC;;;;;;;;;OASG;IACH,IAAI,MAAM,IAAI,aAAa,CAAC;IAC5B,IAAI,MAAM,CAAC,KAAK,EAAE,aAAa,EAAE;IAEjC,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,OAAO,CAAC;IACxB,IAAI,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE;IAE7B,2EAA2E;IAC3E,IAAI,eAAe,IAAI,MAAM,CAAC;IAC9B,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE;IAEnC,6EAA6E;IAC7E,IAAI,KAAK,IAAI,MAAM,CAAC;IACpB,IAAI,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE;IAIrB;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IAIhD;;;;;OAKG;IACH,aAAa,IAAI,OAAO;IAExB;;;;OAIG;IACH,cAAc,IAAI,OAAO;IAEzB;;;;OAIG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIxC;;;;;;;;;;;;;;;;;;OAkBG;IAKH,SAAS,CAAC,aAAa,IAAI,OAAO,CAAC,kBAAkB,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EAC7D,IAAI,EAAE,CAAC,GACN,CAAC,GAAG,WAAW,CAAC,iBAAiB,CAAC,CA0FpC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Shared base class for Vi web components.
|
|
4
|
+
* Keep this thin — behaviour is added via mixins (FocusableMixin, FocusTrapMixin).
|
|
5
|
+
*/
|
|
6
|
+
export declare class ViElement extends LitElement {
|
|
7
|
+
}
|
|
8
|
+
/** Component size scale */
|
|
9
|
+
export type ViSize = 'sm' | 'md' | 'lg';
|
|
10
|
+
/** Semantic status — maps to colour tokens (success, warning, danger, info, neutral) */
|
|
11
|
+
export type ViStatus = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
|
|
12
|
+
//# sourceMappingURL=vi-element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vi-element.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/base/vi-element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAEjC;;;GAGG;AACH,qBAAa,SAAU,SAAQ,UAAU;CAAG;AAE5C,2BAA2B;AAC3B,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAExC,wFAAwF;AACxF,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/web-components/src/button/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
|