@sb1/indeks-web 0.3.1 → 0.5.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 +8 -0
- package/README.md +15 -1
- package/dist/npm/index.d.ts +1 -1
- package/dist/npm/index.js +175 -6
- package/dist/npm/lib/components/field/IxField.d.ts +5 -0
- package/dist/npm/lib/tooltip/tooltip.d.ts +1 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -4,12 +4,16 @@ Web components for Indeks designsystemet. Komponenter som fungerer i alle rammev
|
|
|
4
4
|
|
|
5
5
|
## 📦 Del av Indeks
|
|
6
6
|
|
|
7
|
-
Denne pakken er en del av [Indeks designsystemet](https://github.com/SpareBank1/indeks).
|
|
7
|
+
Denne pakken er en del av [Indeks designsystemet](https://github.com/SpareBank1/indeks) og er grunnlaget for komponent-atferden i systemet. Den kreves også i React-prosjekter — `@sb1/indeks-react` er et tynt lag som rendrer `<ix-*>`-elementer, så web components-scriptet fra CDN må lastes i alle Indeks-prosjekter.
|
|
8
|
+
|
|
9
|
+
Komponentene bruker CSS-klasser fra `@sb1/indeks-css`, som må lastes separat.
|
|
8
10
|
|
|
9
11
|
## 📥 Installasjon
|
|
10
12
|
|
|
11
13
|
### Via CDN (anbefalt)
|
|
12
14
|
|
|
15
|
+
CDN er anbefalt fordi URL-en deles på tvers av SB1-applikasjoner. Nettleseren kan gjenbruke samme cachede CSS og JS, slik at brukere som allerede har besøkt en annen SB1-app slipper å laste ned det samme på nytt.
|
|
16
|
+
|
|
13
17
|
```html
|
|
14
18
|
<!-- Last inn CSS -->
|
|
15
19
|
<link rel="stylesheet" href="https://cdn.sparebank1.no/indeks/css/<versjon>.css" />
|
|
@@ -18,6 +22,16 @@ Denne pakken er en del av [Indeks designsystemet](https://github.com/SpareBank1/
|
|
|
18
22
|
<script type="module" src="https://cdn.sparebank1.no/indeks/web/<versjon>/index.js"></script>
|
|
19
23
|
```
|
|
20
24
|
|
|
25
|
+
#### TypeScript-typer (valgfritt)
|
|
26
|
+
|
|
27
|
+
CDN-scriptet inneholder ingen typedeklarasjoner. Installer npm-pakken som `devDependency` for å få typer for `<ix-field>` m.fl. både i TypeScript og JSX:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save-dev @sb1/indeks-web
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pakken registrerer typene globalt via `HTMLElementTagNameMap` og JSX — ingen import nødvendig i koden.
|
|
34
|
+
|
|
21
35
|
Etter at scriptet er lastet inn er alle komponenter tilgjengelige som HTML-elementer:
|
|
22
36
|
|
|
23
37
|
```html
|
package/dist/npm/index.d.ts
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.129.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.129.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.129.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.129.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,
|
|
@@ -38,6 +38,7 @@ function _defineProperty(e, r, t) {
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region lib/components/field/IxField.ts
|
|
40
40
|
var fieldCounter = 0;
|
|
41
|
+
var ICON_URL = "https://cdn.sparebank1.no/icons/info.svg";
|
|
41
42
|
var IxField = class extends HTMLElement {
|
|
42
43
|
constructor(..._args) {
|
|
43
44
|
super(..._args);
|
|
@@ -46,8 +47,16 @@ var IxField = class extends HTMLElement {
|
|
|
46
47
|
_defineProperty(this, "_charCountListener", null);
|
|
47
48
|
_defineProperty(this, "_charCountControl", null);
|
|
48
49
|
}
|
|
50
|
+
static get observedAttributes() {
|
|
51
|
+
return [
|
|
52
|
+
"tooltip",
|
|
53
|
+
"tooltip-label",
|
|
54
|
+
"tooltip-placement"
|
|
55
|
+
];
|
|
56
|
+
}
|
|
49
57
|
connectedCallback() {
|
|
50
58
|
this._wire();
|
|
59
|
+
this._syncTooltip();
|
|
51
60
|
}
|
|
52
61
|
disconnectedCallback() {
|
|
53
62
|
this._observer?.disconnect();
|
|
@@ -56,6 +65,11 @@ var IxField = class extends HTMLElement {
|
|
|
56
65
|
this._stateObserver = null;
|
|
57
66
|
this._teardownCharCount();
|
|
58
67
|
}
|
|
68
|
+
attributeChangedCallback(name, _oldValue, _newValue) {
|
|
69
|
+
if (name === "tooltip" || name === "tooltip-label" || name === "tooltip-placement") {
|
|
70
|
+
if (this.isConnected) this._syncTooltip();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
_wire() {
|
|
60
74
|
const control = this.querySelector("input, select, textarea");
|
|
61
75
|
const label = this.querySelector("label");
|
|
@@ -90,10 +104,14 @@ var IxField = class extends HTMLElement {
|
|
|
90
104
|
"minlength"
|
|
91
105
|
]
|
|
92
106
|
});
|
|
93
|
-
if (control instanceof HTMLInputElement && control.type === "number") console.warn("[ix-field] Unngå type=\"number\" — bruk inputMode=\"numeric\" i stedet.");
|
|
94
107
|
if (!control.id) control.id = `ix-field-${++fieldCounter}`;
|
|
108
|
+
if (label && !label.closest(".ix-field__label-row")) {
|
|
109
|
+
const labelRow = document.createElement("div");
|
|
110
|
+
labelRow.className = "ix-field__label-row";
|
|
111
|
+
label.parentNode.insertBefore(labelRow, label);
|
|
112
|
+
labelRow.appendChild(label);
|
|
113
|
+
}
|
|
95
114
|
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.");
|
|
97
115
|
const describedBy = [];
|
|
98
116
|
if (description) {
|
|
99
117
|
if (!description.id) description.id = `${control.id}-description`;
|
|
@@ -167,8 +185,159 @@ var IxField = class extends HTMLElement {
|
|
|
167
185
|
const readOnly = "readOnly" in control ? control.readOnly : false;
|
|
168
186
|
this.toggleAttribute("data-readonly", readOnly);
|
|
169
187
|
}
|
|
188
|
+
_syncTooltip() {
|
|
189
|
+
const content = this.getAttribute("tooltip");
|
|
190
|
+
if (content) this._setupTooltipBtn(content);
|
|
191
|
+
else this._teardownTooltipBtn();
|
|
192
|
+
}
|
|
193
|
+
_setupTooltipBtn(content) {
|
|
194
|
+
const tooltipLabel = this.getAttribute("tooltip-label");
|
|
195
|
+
const existingBtn = this.querySelector(".ix-field__tooltip-btn");
|
|
196
|
+
if (existingBtn) {
|
|
197
|
+
existingBtn.setAttribute("data-tooltip", content);
|
|
198
|
+
existingBtn.setAttribute("data-tooltip-placement", this.getAttribute("tooltip-placement") ?? "top");
|
|
199
|
+
if (tooltipLabel) existingBtn.setAttribute("aria-label", tooltipLabel);
|
|
200
|
+
else existingBtn.removeAttribute("aria-label");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const labelRow = this.querySelector(".ix-field__label-row");
|
|
204
|
+
if (!labelRow) return;
|
|
205
|
+
const btn = document.createElement("button");
|
|
206
|
+
btn.type = "button";
|
|
207
|
+
btn.className = "ix-field__tooltip-btn";
|
|
208
|
+
btn.setAttribute("data-tooltip", content);
|
|
209
|
+
btn.setAttribute("data-tooltip-placement", this.getAttribute("tooltip-placement") ?? "top");
|
|
210
|
+
if (tooltipLabel) btn.setAttribute("aria-label", tooltipLabel);
|
|
211
|
+
const icon = document.createElement("span");
|
|
212
|
+
icon.className = "ix-icon";
|
|
213
|
+
icon.setAttribute("aria-hidden", "true");
|
|
214
|
+
icon.style.maskImage = `url(${ICON_URL})`;
|
|
215
|
+
btn.appendChild(icon);
|
|
216
|
+
labelRow.appendChild(btn);
|
|
217
|
+
}
|
|
218
|
+
_teardownTooltipBtn() {
|
|
219
|
+
this.querySelector(".ix-field__tooltip-btn")?.remove();
|
|
220
|
+
}
|
|
170
221
|
};
|
|
171
222
|
//#endregion
|
|
223
|
+
//#region lib/tooltip/tooltip.ts
|
|
224
|
+
var TOOLTIP_ID = "ix-tooltip-singleton";
|
|
225
|
+
var ATTR = "data-tooltip";
|
|
226
|
+
var ATTR_PLACEMENT = "data-tooltip-placement";
|
|
227
|
+
var DELAY_HOVER = 300;
|
|
228
|
+
var tip;
|
|
229
|
+
var source;
|
|
230
|
+
var hoverTimer = 0;
|
|
231
|
+
function getOrCreateTip() {
|
|
232
|
+
if (!tip) {
|
|
233
|
+
tip = document.createElement("div");
|
|
234
|
+
tip.id = TOOLTIP_ID;
|
|
235
|
+
tip.className = "ix-tooltip";
|
|
236
|
+
tip.setAttribute("role", "tooltip");
|
|
237
|
+
tip.hidden = true;
|
|
238
|
+
}
|
|
239
|
+
if (!tip.isConnected) document.body.appendChild(tip);
|
|
240
|
+
return tip;
|
|
241
|
+
}
|
|
242
|
+
function place(trigger, panel, placement) {
|
|
243
|
+
const r = trigger.getBoundingClientRect();
|
|
244
|
+
const vw = window.innerWidth;
|
|
245
|
+
const vh = window.innerHeight;
|
|
246
|
+
panel.hidden = false;
|
|
247
|
+
panel.style.visibility = "hidden";
|
|
248
|
+
const pw = panel.offsetWidth;
|
|
249
|
+
const ph = panel.offsetHeight;
|
|
250
|
+
const arrowSize = 6;
|
|
251
|
+
const gap = arrowSize + 4;
|
|
252
|
+
const axis = placement.startsWith("top") ? "top" : placement.startsWith("bottom") ? "bottom" : placement;
|
|
253
|
+
let resolvedAxis = axis;
|
|
254
|
+
if (axis === "top" && r.top - ph - gap < 0) resolvedAxis = "bottom";
|
|
255
|
+
else if (axis === "bottom" && r.bottom + ph + gap > vh) resolvedAxis = "top";
|
|
256
|
+
else if (axis === "left" && r.left - pw - gap < 0) resolvedAxis = "right";
|
|
257
|
+
else if (axis === "right" && r.right + pw + gap > vw) resolvedAxis = "left";
|
|
258
|
+
const align = placement.endsWith("-start") ? "start" : placement.endsWith("-end") ? "end" : "center";
|
|
259
|
+
const resolved = align === "center" ? resolvedAxis : `${resolvedAxis}-${align}`;
|
|
260
|
+
panel.setAttribute("data-placement", resolved);
|
|
261
|
+
const triggerCenterX = r.left + r.width / 2;
|
|
262
|
+
const triggerCenterY = r.top + r.height / 2;
|
|
263
|
+
let top;
|
|
264
|
+
let left;
|
|
265
|
+
if (resolvedAxis === "top" || resolvedAxis === "bottom") {
|
|
266
|
+
top = resolvedAxis === "top" ? r.top - ph - gap : r.bottom + gap;
|
|
267
|
+
left = align === "start" ? r.left : align === "end" ? r.right - pw : triggerCenterX - pw / 2;
|
|
268
|
+
} else {
|
|
269
|
+
left = resolvedAxis === "left" ? r.left - pw - gap : r.right + gap;
|
|
270
|
+
top = triggerCenterY - ph / 2;
|
|
271
|
+
}
|
|
272
|
+
const clampedLeft = Math.max(8, Math.min(left, vw - pw - 8));
|
|
273
|
+
const clampedTop = Math.max(8, Math.min(top, vh - ph - 8));
|
|
274
|
+
if (resolvedAxis === "top" || resolvedAxis === "bottom") {
|
|
275
|
+
const padding = parseFloat(getComputedStyle(panel).paddingInlineStart) || 12;
|
|
276
|
+
const half = arrowSize / 2;
|
|
277
|
+
const rawX = triggerCenterX - clampedLeft;
|
|
278
|
+
const arrowX = align === "start" ? Math.max(rawX, padding + half) : align === "end" ? Math.min(rawX, pw - padding - half) : rawX;
|
|
279
|
+
panel.style.setProperty("--_arrow-x", `${arrowX}px`);
|
|
280
|
+
panel.style.removeProperty("--_arrow-y");
|
|
281
|
+
} else {
|
|
282
|
+
panel.style.setProperty("--_arrow-y", `${triggerCenterY - clampedTop}px`);
|
|
283
|
+
panel.style.removeProperty("--_arrow-x");
|
|
284
|
+
}
|
|
285
|
+
panel.style.top = `${clampedTop}px`;
|
|
286
|
+
panel.style.left = `${clampedLeft}px`;
|
|
287
|
+
panel.style.visibility = "";
|
|
288
|
+
}
|
|
289
|
+
function show(trigger) {
|
|
290
|
+
const content = trigger.getAttribute(ATTR);
|
|
291
|
+
if (!content) return;
|
|
292
|
+
const placement = trigger.getAttribute(ATTR_PLACEMENT) ?? "top";
|
|
293
|
+
const panel = getOrCreateTip();
|
|
294
|
+
if (source && source !== trigger) hide();
|
|
295
|
+
source = trigger;
|
|
296
|
+
panel.textContent = content;
|
|
297
|
+
trigger.setAttribute("aria-describedby", TOOLTIP_ID);
|
|
298
|
+
place(trigger, panel, placement);
|
|
299
|
+
panel.hidden = false;
|
|
300
|
+
}
|
|
301
|
+
function hide() {
|
|
302
|
+
clearTimeout(hoverTimer);
|
|
303
|
+
if (tip) tip.hidden = true;
|
|
304
|
+
if (source) {
|
|
305
|
+
source.removeAttribute("aria-describedby");
|
|
306
|
+
source = void 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function handleMouseover(e) {
|
|
310
|
+
const target = e.target.closest?.(`[${ATTR}]`);
|
|
311
|
+
if (!target) return;
|
|
312
|
+
if (target === source) return;
|
|
313
|
+
clearTimeout(hoverTimer);
|
|
314
|
+
hoverTimer = window.setTimeout(() => show(target), DELAY_HOVER);
|
|
315
|
+
}
|
|
316
|
+
function handleMouseout(e) {
|
|
317
|
+
const related = e.relatedTarget;
|
|
318
|
+
if (tip && related && tip.contains(related)) return;
|
|
319
|
+
clearTimeout(hoverTimer);
|
|
320
|
+
if (source && !source.contains(related)) hide();
|
|
321
|
+
}
|
|
322
|
+
function handleFocusin(e) {
|
|
323
|
+
const target = e.target.closest?.(`[${ATTR}]`);
|
|
324
|
+
if (!target) return;
|
|
325
|
+
clearTimeout(hoverTimer);
|
|
326
|
+
show(target);
|
|
327
|
+
}
|
|
328
|
+
function handleFocusout(e) {
|
|
329
|
+
const related = e.relatedTarget;
|
|
330
|
+
if (source && !source.contains(related)) hide();
|
|
331
|
+
}
|
|
332
|
+
function handleKeydown(e) {
|
|
333
|
+
if (e.key === "Escape" && source) hide();
|
|
334
|
+
}
|
|
335
|
+
document.addEventListener("mouseover", handleMouseover);
|
|
336
|
+
document.addEventListener("mouseout", handleMouseout);
|
|
337
|
+
document.addEventListener("focusin", handleFocusin);
|
|
338
|
+
document.addEventListener("focusout", handleFocusout);
|
|
339
|
+
document.addEventListener("keydown", handleKeydown);
|
|
340
|
+
//#endregion
|
|
172
341
|
//#region index.ts
|
|
173
342
|
customElements.define("ix-field", IxField);
|
|
174
343
|
//#endregion
|
|
@@ -81,12 +81,17 @@ export declare class IxField extends HTMLElement {
|
|
|
81
81
|
private _stateObserver;
|
|
82
82
|
private _charCountListener;
|
|
83
83
|
private _charCountControl;
|
|
84
|
+
static get observedAttributes(): string[];
|
|
84
85
|
connectedCallback(): void;
|
|
85
86
|
disconnectedCallback(): void;
|
|
87
|
+
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void;
|
|
86
88
|
private _wire;
|
|
87
89
|
private _setupCharCount;
|
|
88
90
|
private _teardownCharCount;
|
|
89
91
|
private _updateCharCount;
|
|
90
92
|
private _syncInvalid;
|
|
91
93
|
private _syncControlState;
|
|
94
|
+
private _syncTooltip;
|
|
95
|
+
private _setupTooltipBtn;
|
|
96
|
+
private _teardownTooltipBtn;
|
|
92
97
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sb1/indeks-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web components for Indeks design system",
|
|
6
6
|
"homepage": "https://design.sparebank1.no",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"author": "",
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^25.
|
|
44
|
-
"jsdom": "^29.
|
|
45
|
-
"typescript": "~
|
|
46
|
-
"vite": "^8.0.
|
|
47
|
-
"vite-plugin-dts": "^
|
|
48
|
-
"vitest": "^4.1.
|
|
43
|
+
"@types/node": "^25.6.2",
|
|
44
|
+
"jsdom": "^29.1.1",
|
|
45
|
+
"typescript": "~6.0.3",
|
|
46
|
+
"vite": "^8.0.12",
|
|
47
|
+
"vite-plugin-dts": "^5.0.0",
|
|
48
|
+
"vitest": "^4.1.5"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"dev": "echo not implemented",
|