@sb1/indeks-web 0.2.21 → 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/CHANGELOG.md +9 -0
- package/dist/npm/index.js +90 -7
- package/dist/npm/lib/components/field/IxField.d.ts +14 -1
- package/package.json +9 -5
package/CHANGELOG.md
CHANGED
package/dist/npm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.124.0/helpers/typeof.js
|
|
2
2
|
function _typeof(o) {
|
|
3
3
|
"@babel/helpers - typeof";
|
|
4
4
|
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
@@ -8,7 +8,7 @@ function _typeof(o) {
|
|
|
8
8
|
}, _typeof(o);
|
|
9
9
|
}
|
|
10
10
|
//#endregion
|
|
11
|
-
//#region \0@oxc-project+runtime@0.
|
|
11
|
+
//#region \0@oxc-project+runtime@0.124.0/helpers/toPrimitive.js
|
|
12
12
|
function toPrimitive(t, r) {
|
|
13
13
|
if ("object" != _typeof(t) || !t) return t;
|
|
14
14
|
var e = t[Symbol.toPrimitive];
|
|
@@ -20,13 +20,13 @@ function toPrimitive(t, r) {
|
|
|
20
20
|
return ("string" === r ? String : Number)(t);
|
|
21
21
|
}
|
|
22
22
|
//#endregion
|
|
23
|
-
//#region \0@oxc-project+runtime@0.
|
|
23
|
+
//#region \0@oxc-project+runtime@0.124.0/helpers/toPropertyKey.js
|
|
24
24
|
function toPropertyKey(t) {
|
|
25
25
|
var i = toPrimitive(t, "string");
|
|
26
26
|
return "symbol" == _typeof(i) ? i : i + "";
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
|
-
//#region \0@oxc-project+runtime@0.
|
|
29
|
+
//#region \0@oxc-project+runtime@0.124.0/helpers/defineProperty.js
|
|
30
30
|
function _defineProperty(e, r, t) {
|
|
31
31
|
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
32
32
|
value: t,
|
|
@@ -42,6 +42,9 @@ var IxField = class extends HTMLElement {
|
|
|
42
42
|
constructor(..._args) {
|
|
43
43
|
super(..._args);
|
|
44
44
|
_defineProperty(this, "_observer", null);
|
|
45
|
+
_defineProperty(this, "_stateObserver", null);
|
|
46
|
+
_defineProperty(this, "_charCountListener", null);
|
|
47
|
+
_defineProperty(this, "_charCountControl", null);
|
|
45
48
|
}
|
|
46
49
|
connectedCallback() {
|
|
47
50
|
this._wire();
|
|
@@ -49,15 +52,48 @@ var IxField = class extends HTMLElement {
|
|
|
49
52
|
disconnectedCallback() {
|
|
50
53
|
this._observer?.disconnect();
|
|
51
54
|
this._observer = null;
|
|
55
|
+
this._stateObserver?.disconnect();
|
|
56
|
+
this._stateObserver = null;
|
|
57
|
+
this._teardownCharCount();
|
|
52
58
|
}
|
|
53
59
|
_wire() {
|
|
54
60
|
const control = this.querySelector("input, select, textarea");
|
|
55
61
|
const label = this.querySelector("label");
|
|
56
62
|
const description = this.querySelector("[data-field=\"description\"]");
|
|
57
63
|
const error = this.querySelector("[data-field=\"error\"]");
|
|
58
|
-
if (!control)
|
|
64
|
+
if (!control) {
|
|
65
|
+
console.info("[ix-field] Fant ingen <input>, <select> eller <textarea>. ARIA-koblinger ble ikke satt opp.");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this._syncControlState(control);
|
|
69
|
+
this._stateObserver = new MutationObserver((mutations) => {
|
|
70
|
+
this._syncControlState(control);
|
|
71
|
+
if (mutations.some((m) => m.attributeName === "maxlength" || m.attributeName === "minlength") && (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement)) {
|
|
72
|
+
this._teardownCharCount();
|
|
73
|
+
const charCountEl = this._setupCharCount(control);
|
|
74
|
+
if (charCountEl) {
|
|
75
|
+
const existing = control.getAttribute("aria-describedby") ?? "";
|
|
76
|
+
const ids = existing ? existing.split(" ") : [];
|
|
77
|
+
if (!ids.includes(charCountEl.id)) {
|
|
78
|
+
ids.push(charCountEl.id);
|
|
79
|
+
control.setAttribute("aria-describedby", ids.join(" "));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this._stateObserver.observe(control, {
|
|
85
|
+
attributes: true,
|
|
86
|
+
attributeFilter: [
|
|
87
|
+
"disabled",
|
|
88
|
+
"readonly",
|
|
89
|
+
"maxlength",
|
|
90
|
+
"minlength"
|
|
91
|
+
]
|
|
92
|
+
});
|
|
93
|
+
if (control instanceof HTMLInputElement && control.type === "number") console.warn("[ix-field] Unngå type=\"number\" — bruk inputMode=\"numeric\" i stedet.");
|
|
59
94
|
if (!control.id) control.id = `ix-field-${++fieldCounter}`;
|
|
60
95
|
if (label && !label.htmlFor) label.htmlFor = control.id;
|
|
96
|
+
if (!label && !control.getAttribute("aria-label") && !control.getAttribute("aria-labelledby")) console.warn("[ix-field] Kontrollen mangler tilgjengelig navn. Legg til <label>, aria-label eller aria-labelledby. Synlig label er anbefalt.");
|
|
61
97
|
const describedBy = [];
|
|
62
98
|
if (description) {
|
|
63
99
|
if (!description.id) description.id = `${control.id}-description`;
|
|
@@ -65,7 +101,6 @@ var IxField = class extends HTMLElement {
|
|
|
65
101
|
}
|
|
66
102
|
if (error) {
|
|
67
103
|
if (!error.id) error.id = `${control.id}-error`;
|
|
68
|
-
error.setAttribute("role", "alert");
|
|
69
104
|
error.setAttribute("aria-live", "polite");
|
|
70
105
|
describedBy.push(error.id);
|
|
71
106
|
this._observer = new MutationObserver(() => {
|
|
@@ -78,11 +113,59 @@ var IxField = class extends HTMLElement {
|
|
|
78
113
|
});
|
|
79
114
|
this._syncInvalid(control, error);
|
|
80
115
|
}
|
|
116
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
|
|
117
|
+
const charCountEl = this._setupCharCount(control);
|
|
118
|
+
if (charCountEl) describedBy.push(charCountEl.id);
|
|
119
|
+
}
|
|
81
120
|
if (describedBy.length > 0) control.setAttribute("aria-describedby", describedBy.join(" "));
|
|
121
|
+
const prefix = this.querySelector("[data-field=\"prefix\"]");
|
|
122
|
+
const suffix = this.querySelector("[data-field=\"suffix\"]");
|
|
123
|
+
prefix?.setAttribute("aria-hidden", "true");
|
|
124
|
+
suffix?.setAttribute("aria-hidden", "true");
|
|
125
|
+
const textField = this.querySelector(".ix-text-field");
|
|
126
|
+
if (textField) {
|
|
127
|
+
textField.toggleAttribute("data-has-prefix", !!prefix);
|
|
128
|
+
textField.toggleAttribute("data-has-suffix", !!suffix);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
_setupCharCount(control) {
|
|
132
|
+
const min = control.getAttribute("minlength");
|
|
133
|
+
const max = control.getAttribute("maxlength");
|
|
134
|
+
if (!min && !max) return null;
|
|
135
|
+
const el = document.createElement("span");
|
|
136
|
+
el.id = `${control.id}-char-count`;
|
|
137
|
+
el.setAttribute("data-field", "char-count");
|
|
138
|
+
el.setAttribute("aria-live", "off");
|
|
139
|
+
this._updateCharCount(el, control, min, max);
|
|
140
|
+
const error = this.querySelector("[data-field=\"error\"]");
|
|
141
|
+
if (error?.parentNode) error.parentNode.insertBefore(el, error.nextSibling);
|
|
142
|
+
else this.appendChild(el);
|
|
143
|
+
this._charCountControl = control;
|
|
144
|
+
this._charCountListener = () => this._updateCharCount(el, control, min, max);
|
|
145
|
+
control.addEventListener("input", this._charCountListener);
|
|
146
|
+
return el;
|
|
147
|
+
}
|
|
148
|
+
_teardownCharCount() {
|
|
149
|
+
if (this._charCountListener && this._charCountControl) this._charCountControl.removeEventListener("input", this._charCountListener);
|
|
150
|
+
this._charCountListener = null;
|
|
151
|
+
this._charCountControl = null;
|
|
152
|
+
this.querySelector("[data-field=\"char-count\"]")?.remove();
|
|
153
|
+
}
|
|
154
|
+
_updateCharCount(el, control, min, max) {
|
|
155
|
+
const current = control.value.length;
|
|
156
|
+
if (max) el.textContent = `${current}/${max}`;
|
|
157
|
+
else if (min) el.textContent = `${current} tegn (minimum ${min})`;
|
|
82
158
|
}
|
|
83
159
|
_syncInvalid(control, error) {
|
|
84
|
-
|
|
160
|
+
const hasError = !!error.textContent?.trim();
|
|
161
|
+
if (hasError) control.setAttribute("aria-invalid", "true");
|
|
85
162
|
else control.removeAttribute("aria-invalid");
|
|
163
|
+
control.closest(".ix-dropdown")?.toggleAttribute("data-invalid", hasError);
|
|
164
|
+
}
|
|
165
|
+
_syncControlState(control) {
|
|
166
|
+
this.toggleAttribute("data-disabled", control.disabled);
|
|
167
|
+
const readOnly = "readOnly" in control ? control.readOnly : false;
|
|
168
|
+
this.toggleAttribute("data-readonly", readOnly);
|
|
86
169
|
}
|
|
87
170
|
};
|
|
88
171
|
//#endregion
|
|
@@ -22,13 +22,17 @@
|
|
|
22
22
|
* 2. Setter `aria-describedby` på input med IDer til description og
|
|
23
23
|
* error-elementet, slik at skjermlesere leser dem opp etter label og verdi.
|
|
24
24
|
*
|
|
25
|
-
* 3. Setter `
|
|
25
|
+
* 3. Setter `aria-live="polite"` på error-elementet.
|
|
26
26
|
* Error-elementet skal alltid ligge i DOM — tomt betyr ingen feil.
|
|
27
27
|
* Dette er et krav fra ARIA Live Regions: elementet må eksistere i DOM
|
|
28
28
|
* *før* innholdet settes inn for at skjermlesere skal fange opp endringen.
|
|
29
29
|
* Hadde vi injisert elementet dynamisk ville mange skjermlesere gått glipp
|
|
30
30
|
* av meldingen.
|
|
31
31
|
*
|
|
32
|
+
* Vi bruker *ikke* `role="alert"` fordi det impliserer `aria-live="assertive"`
|
|
33
|
+
* som avbryter brukeren. Feilmeldinger i skjema er viktige, men ikke så
|
|
34
|
+
* kritiske at de bør avbryte — `polite` venter til skjermleseren er ferdig.
|
|
35
|
+
*
|
|
32
36
|
* 4. Bruker MutationObserver på error-elementet. Når innholdet endres:
|
|
33
37
|
* - Ikke-tomt → `aria-invalid="true"` på input (visuell og semantisk feil)
|
|
34
38
|
* - Tomt → `aria-invalid` fjernes
|
|
@@ -42,6 +46,8 @@
|
|
|
42
46
|
* - `input | select | textarea` → første native form-kontroll
|
|
43
47
|
* - `[data-field="description"]` → hjelpetekst
|
|
44
48
|
* - `[data-field="error"]` → feilmelding (alltid til stede, kan være tom)
|
|
49
|
+
* - `[data-field="prefix"]` → visuell prefix (skjules med aria-hidden)
|
|
50
|
+
* - `[data-field="suffix"]` → visuell suffix (skjules med aria-hidden)
|
|
45
51
|
*
|
|
46
52
|
* `data-field` brukes fremfor `slot`-attributtet selv om Shadow DOM ikke er i
|
|
47
53
|
* bruk. `slot` er et reservert nøkkelord knytt til Shadow DOM og ville vært
|
|
@@ -72,8 +78,15 @@
|
|
|
72
78
|
*/
|
|
73
79
|
export declare class IxField extends HTMLElement {
|
|
74
80
|
private _observer;
|
|
81
|
+
private _stateObserver;
|
|
82
|
+
private _charCountListener;
|
|
83
|
+
private _charCountControl;
|
|
75
84
|
connectedCallback(): void;
|
|
76
85
|
disconnectedCallback(): void;
|
|
77
86
|
private _wire;
|
|
87
|
+
private _setupCharCount;
|
|
88
|
+
private _teardownCharCount;
|
|
89
|
+
private _updateCharCount;
|
|
78
90
|
private _syncInvalid;
|
|
91
|
+
private _syncControlState;
|
|
79
92
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sb1/indeks-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web components for Indeks design system",
|
|
6
6
|
"homepage": "https://design.sparebank1.no",
|
|
@@ -40,16 +40,20 @@
|
|
|
40
40
|
"author": "",
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^25.
|
|
43
|
+
"@types/node": "^25.5.0",
|
|
44
|
+
"jsdom": "^29.0.1",
|
|
44
45
|
"typescript": "~5.9.3",
|
|
45
|
-
"vite": "^8.0.
|
|
46
|
-
"vite-plugin-dts": "^4.5.4"
|
|
46
|
+
"vite": "^8.0.5",
|
|
47
|
+
"vite-plugin-dts": "^4.5.4",
|
|
48
|
+
"vitest": "^4.1.2"
|
|
47
49
|
},
|
|
48
50
|
"scripts": {
|
|
51
|
+
"dev": "echo not implemented",
|
|
49
52
|
"build": "rm -rf ./dist && pnpm run build:npm",
|
|
50
53
|
"build:npm": "rm -rf ./dist/npm && vite build --mode npm",
|
|
51
54
|
"build:cdn": "rm -rf ./dist/cdn && vite build --mode cdn",
|
|
52
55
|
"lint": "eslint .",
|
|
53
|
-
"
|
|
56
|
+
"update-packages": "echo not implemented",
|
|
57
|
+
"test": "vitest"
|
|
54
58
|
}
|
|
55
59
|
}
|