@sb1/indeks-web 0.24.0 → 0.26.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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @sb1/indeks-web
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 250261e: Én hook per del og ingen `:where()` i accordion, read-more og tabs (ADR-DS-009).
8
+
9
+ Breaking: Accordion styler `<details>` og `<summary>` direkte, og innholdet må ha `ix-accordion__content`. Tabs styler `<ix-tab-list>`, `<ix-tab>` og `<ix-tab-panel>` inni `.ix-tabs`. `ix-accordion__item`, `ix-accordion__header`, `ix-tabs__list`, `ix-tabs__tab` og `ix-tabs__panel` styles ikke lenger, og React setter dem ikke.
10
+ - 140b0cb: Én hook per del og ingen `:where()` i checkbox, checkbox-group, chip og modal (ADR-DS-009).
11
+
12
+ Breaking: `ix-checkbox--disabled` er fjernet. Bruk `disabled` på inputen. `ix-checkbox__input` og `ix-checkbox__label` styles ikke lenger, men gjør ingen skade i markupen.
13
+ - 04e6c71: ProgressBar viser nå status som i Figma. I `success` og `error` ser støtteteksten under baren ut som en ValidationMessage, med statusikonet foran. Ikonet vises også uten støttetekst.
14
+
15
+ **Breaking:** Statusikonet er flyttet fra headeren til støtteteksten, og feilikonet heter nå `close` i stedet for `priority_high`. Har du stylet `.ix-progress-bar__header ix-icon`, eller lett etter ikonet med navn, må du bruke `.ix-progress-bar__support ix-icon[data-badge]`. Meldingen bruker ikke `data-field="error"`, så et `<ix-field>` rundt en ProgressBar tar den ikke for sin egen feilmelding.
16
+ - 636e79c: Én hook per del og ingen `:where()` i radio-group (ADR-DS-009).
17
+
18
+ Breaking: `ix-radio-button`, `ix-radio-button__input` og `ix-radio-button__label` er fjernet. Radio styles bare inni `ix-radio-group` eller `.ix-radio-group`, og klassene gjør ingen skade i markupen. `ix-radio-group` finner labelen i inputens forelder.
19
+ - 04e6c71: ValidationMessage har fått en suksessvariant: `data-status="success"` i HTML og `status="success"` i React gir grønn tekst og en sirkel med hake. `<ix-field>`, `<ix-checkbox-group>`, `<ix-radio-group>` og `<ix-phone-number-field>` setter ikke `aria-invalid` for en suksessmelding, og `<ix-phone-number-field>` setter heller ikke `data-state="error"`.
20
+
21
+ Feilmeldingen i `<ix-phone-number-field>` har nå samme avstand mellom ikon og tekst som i de andre skjemafeltene (`xs` i stedet for `2xs`).
22
+
23
+ ### Patch Changes
24
+
25
+ - 329f308: Én hook per del og ingen `:where()` i combobox og date-field (ADR-DS-009).
26
+
27
+ Breaking: chips-wrapperen i combobox styles fra `data-field="chips"`, ikke `ix-combobox__chips`. Toggle, listbox og option styles bare fra BEM-klassene, ikke fra `role` eller `.ix-text-field > button`. `ix-combobox` finner inputen med `.ix-text-field > input`.
28
+ - dc9139a: Modal: scroll-låsen på `<body>` slippes nå også når en åpen modal fjernes fra DOM-en, for eksempel når forelderen fjerner den i `onOpenChange` eller ved ruteskifte. Før ble siden stående uten scroll.
29
+
30
+ ## 0.25.0
31
+
32
+ No changes in this release.
33
+
3
34
  ## 0.24.0
4
35
 
5
36
  No changes in this release.
package/dist/npm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.149.0/helpers/esm/typeof.js
1
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/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) {
@@ -7,8 +7,7 @@ function _typeof(o) {
7
7
  return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
8
8
  }, _typeof(o);
9
9
  }
10
- //#endregion
11
- //#region \0@oxc-project+runtime@0.149.0/helpers/esm/toPrimitive.js
10
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPrimitive.js
12
11
  function toPrimitive(t, r) {
13
12
  if ("object" != _typeof(t) || !t) return t;
14
13
  var e = t[Symbol.toPrimitive];
@@ -19,14 +18,12 @@ function toPrimitive(t, r) {
19
18
  }
20
19
  return ("string" === r ? String : Number)(t);
21
20
  }
22
- //#endregion
23
- //#region \0@oxc-project+runtime@0.149.0/helpers/esm/toPropertyKey.js
21
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPropertyKey.js
24
22
  function toPropertyKey(t) {
25
23
  var i = toPrimitive(t, "string");
26
24
  return "symbol" == _typeof(i) ? i : i + "";
27
25
  }
28
- //#endregion
29
- //#region \0@oxc-project+runtime@0.149.0/helpers/esm/defineProperty.js
26
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/defineProperty.js
30
27
  function _defineProperty(e, r, t) {
31
28
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
32
29
  value: t,
@@ -35,8 +32,7 @@ function _defineProperty(e, r, t) {
35
32
  writable: !0
36
33
  }) : e[r] = t, e;
37
34
  }
38
- //#endregion
39
- //#region lib/components/checkbox-group/IxCheckboxGroup.ts
35
+ // lib/components/checkbox-group/IxCheckboxGroup.ts
40
36
  /**
41
37
  * IxCheckboxGroup — ARIA-lim for checkbox-grupper.
42
38
  *
@@ -163,7 +159,8 @@ var IxCheckboxGroup = class extends HTMLElement {
163
159
  this._errorObserver.observe(error, {
164
160
  childList: true,
165
161
  characterData: true,
166
- subtree: true
162
+ subtree: true,
163
+ attributeFilter: ["data-status"]
167
164
  });
168
165
  this._syncInvalid(error);
169
166
  }
@@ -198,7 +195,7 @@ var IxCheckboxGroup = class extends HTMLElement {
198
195
  if (!input.id) input.id = `ix-checkbox-${++checkboxInputCounter}`;
199
196
  const wrapper = input.closest(".ix-checkbox") ?? input.parentElement;
200
197
  if (wrapper) {
201
- const label = wrapper.querySelector("label, .ix-checkbox__label");
198
+ const label = wrapper.querySelector("label");
202
199
  if (label instanceof HTMLLabelElement && !label.htmlFor) label.htmlFor = input.id;
203
200
  }
204
201
  }
@@ -207,7 +204,7 @@ var IxCheckboxGroup = class extends HTMLElement {
207
204
  this._syncHostStateToInputs();
208
205
  }
209
206
  _syncInvalid(error) {
210
- if (!!error.textContent?.trim()) this.setAttribute("aria-invalid", "true");
207
+ if (!!error.textContent?.trim() && error.dataset.status !== "success") this.setAttribute("aria-invalid", "true");
211
208
  else this.removeAttribute("aria-invalid");
212
209
  }
213
210
  _syncHostStateToInputs() {
@@ -222,8 +219,7 @@ var IxCheckboxGroup = class extends HTMLElement {
222
219
  }
223
220
  }
224
221
  };
225
- //#endregion
226
- //#region lib/components/combobox/IxCombobox.ts
222
+ // lib/components/combobox/IxCombobox.ts
227
223
  /**
228
224
  * IxCombobox — ARIA-lim for en søkbar select med single- og multiselect.
229
225
  *
@@ -269,7 +265,7 @@ var IxCheckboxGroup = class extends HTMLElement {
269
265
  * @example Single
270
266
  * <ix-combobox data-no-hits-text="Ingen treff">
271
267
  * <div class="ix-text-field">
272
- * <input class="ix-text-field__input" placeholder="Søk …" />
268
+ * <input placeholder="Søk …" />
273
269
  * <button class="ix-combobox__toggle"></button>
274
270
  * </div>
275
271
  * <div class="ix-combobox__listbox">
@@ -390,7 +386,7 @@ var IxCombobox = class extends HTMLElement {
390
386
  return this.hasAttribute("disabled") || this.hasAttribute("readonly");
391
387
  }
392
388
  _wire() {
393
- this._input = this.querySelector(".ix-text-field input, input.ix-text-field__input, input");
389
+ this._input = this.querySelector(".ix-text-field > input");
394
390
  this._listbox = this.querySelector(".ix-combobox__listbox");
395
391
  this._toggle = this.querySelector(".ix-combobox__toggle");
396
392
  this._chips = this.querySelector("[data-field=\"chips\"]");
@@ -867,8 +863,7 @@ var IxCombobox = class extends HTMLElement {
867
863
  if (this._results) this._results.textContent = "";
868
864
  }
869
865
  };
870
- //#endregion
871
- //#region lib/components/field/formats.ts
866
+ // lib/components/field/formats.ts
872
867
  /**
873
868
  * Slot-definisjoner for pattern-strenger. Et pattern består av slot-tegn som
874
869
  * brukeren fyller, og faste separatorer (alt annet) som settes inn automatisk.
@@ -1109,8 +1104,7 @@ registerFormat("amount", {
1109
1104
  }),
1110
1105
  live: true
1111
1106
  });
1112
- //#endregion
1113
- //#region lib/components/date-field/IxDateField.ts
1107
+ // lib/components/date-field/IxDateField.ts
1114
1108
  /**
1115
1109
  * IxDateField — datofelt: formatert tekst-input (dd.mm.åååå) + native OS-kalender.
1116
1110
  *
@@ -1319,8 +1313,7 @@ var IxDateField = class extends HTMLElement {
1319
1313
  }
1320
1314
  }
1321
1315
  };
1322
- //#endregion
1323
- //#region lib/components/dropdown/IxDropdown.ts
1316
+ // lib/components/dropdown/IxDropdown.ts
1324
1317
  /**
1325
1318
  * IxDropdown — ARIA-lim for en nedtrekksmeny med handlinger eller navigasjon.
1326
1319
  *
@@ -1816,8 +1809,7 @@ var IxDropdown = class extends HTMLElement {
1816
1809
  }
1817
1810
  }
1818
1811
  };
1819
- //#endregion
1820
- //#region lib/components/field/IxField.ts
1812
+ // lib/components/field/IxField.ts
1821
1813
  /**
1822
1814
  * IxField — tilgjengelighetslim mellom label, input og hjelpetekster.
1823
1815
  *
@@ -2030,7 +2022,8 @@ var IxField = class extends HTMLElement {
2030
2022
  this._observer.observe(error, {
2031
2023
  childList: true,
2032
2024
  characterData: true,
2033
- subtree: true
2025
+ subtree: true,
2026
+ attributeFilter: ["data-status"]
2034
2027
  });
2035
2028
  this._syncInvalid(control, error);
2036
2029
  }
@@ -2197,7 +2190,7 @@ var IxField = class extends HTMLElement {
2197
2190
  this._formatTeardown = null;
2198
2191
  }
2199
2192
  _syncInvalid(control, error) {
2200
- const hasError = !!error.textContent?.trim();
2193
+ const hasError = !!error.textContent?.trim() && error.dataset.status !== "success";
2201
2194
  if (hasError) control.setAttribute("aria-invalid", "true");
2202
2195
  else control.removeAttribute("aria-invalid");
2203
2196
  control.closest(".ix-dropdown")?.toggleAttribute("data-invalid", hasError);
@@ -2245,8 +2238,7 @@ var IxField = class extends HTMLElement {
2245
2238
  }
2246
2239
  };
2247
2240
  _defineProperty(IxField, "registerFormatter", registerFormat);
2248
- //#endregion
2249
- //#region lib/components/icon/IxIcon.ts
2241
+ // lib/components/icon/IxIcon.ts
2250
2242
  var ICON_CDN_BASE = "https://cdn.sparebank1.no/icons";
2251
2243
  var COMMON_ICON_NAMES = [
2252
2244
  "close",
@@ -2432,8 +2424,7 @@ var IxIcon = class extends HTMLElement {
2432
2424
  }
2433
2425
  }
2434
2426
  };
2435
- //#endregion
2436
- //#region lib/components/phone-number-field/countries.ts
2427
+ // lib/components/phone-number-field/countries.ts
2437
2428
  var NORDIC = /* @__PURE__ */ new Set([
2438
2429
  "SE",
2439
2430
  "DK",
@@ -2758,8 +2749,7 @@ function getDefaultCountries(locale = "nb") {
2758
2749
  function normalizeLocale(value) {
2759
2750
  return value === "nn" || value === "en" ? value : "nb";
2760
2751
  }
2761
- //#endregion
2762
- //#region lib/components/phone-number-field/IxPhoneNumberField.ts
2752
+ // lib/components/phone-number-field/IxPhoneNumberField.ts
2763
2753
  /**
2764
2754
  * IxPhoneNumberField — telefonnummer med landvelger.
2765
2755
  *
@@ -2827,7 +2817,7 @@ function normalizeLocale(value) {
2827
2817
  * <div data-field="items">
2828
2818
  * <ix-combobox data-field="country" class="ix-combobox" data-no-hits-text="Ingen treff">
2829
2819
  * <div class="ix-text-field">
2830
- * <input class="ix-text-field__input" aria-label="Landkode" />
2820
+ * <input aria-label="Landkode" />
2831
2821
  * <button type="button" class="ix-combobox__toggle" aria-label="Vis land"></button>
2832
2822
  * </div>
2833
2823
  * <div class="ix-combobox__listbox" hidden></div>
@@ -2929,7 +2919,8 @@ var IxPhoneNumberField = class extends HTMLElement {
2929
2919
  this._errorObserver.observe(error, {
2930
2920
  childList: true,
2931
2921
  characterData: true,
2932
- subtree: true
2922
+ subtree: true,
2923
+ attributeFilter: ["data-status"]
2933
2924
  });
2934
2925
  this._syncInvalid(error);
2935
2926
  } else this.removeAttribute("aria-invalid");
@@ -2958,13 +2949,17 @@ var IxPhoneNumberField = class extends HTMLElement {
2958
2949
  const combobox = this._combobox();
2959
2950
  return Array.from(this.querySelectorAll("input")).find((input) => !combobox?.contains(input)) ?? null;
2960
2951
  }
2952
+ _hasError(error) {
2953
+ return !!error?.textContent?.trim() && error.dataset.status !== "success";
2954
+ }
2961
2955
  _syncInvalid(error) {
2962
- if (!!error.textContent?.trim()) this.setAttribute("aria-invalid", "true");
2956
+ if (this._hasError(error)) this.setAttribute("aria-invalid", "true");
2963
2957
  else this.removeAttribute("aria-invalid");
2964
2958
  this._syncDataState();
2965
2959
  }
2966
2960
  _syncDataState() {
2967
- const state = !!this.querySelector(":scope > [data-field=\"error\"]")?.textContent?.trim() ? "error" : this.hasAttribute("readonly") ? "readonly" : this.hasAttribute("disabled") ? "disabled" : null;
2961
+ const error = this.querySelector(":scope > [data-field=\"error\"]");
2962
+ const state = this._hasError(error) ? "error" : this.hasAttribute("readonly") ? "readonly" : this.hasAttribute("disabled") ? "disabled" : null;
2968
2963
  if (state) this.setAttribute("data-state", state);
2969
2964
  else this.removeAttribute("data-state");
2970
2965
  }
@@ -3096,8 +3091,7 @@ var IxPhoneNumberField = class extends HTMLElement {
3096
3091
  })) if (!input.hasAttribute(attr)) input.setAttribute(attr, value);
3097
3092
  }
3098
3093
  };
3099
- //#endregion
3100
- //#region lib/components/popover/IxPopover.ts
3094
+ // lib/components/popover/IxPopover.ts
3101
3095
  /**
3102
3096
  * IxPopover — ARIA-lim for en popover med kontekstuell informasjon eller handlinger.
3103
3097
  *
@@ -3407,8 +3401,7 @@ var IxPopover = class extends HTMLElement {
3407
3401
  }
3408
3402
  }
3409
3403
  };
3410
- //#endregion
3411
- //#region lib/utils/platform.ts
3404
+ // lib/utils/platform.ts
3412
3405
  /**
3413
3406
  * Plattform-deteksjon for web components.
3414
3407
  *
@@ -3427,13 +3420,12 @@ function isIOS() {
3427
3420
  if (typeof navigator === "undefined") return false;
3428
3421
  return /iPad|iPhone|iPod/.test(navigator.userAgent);
3429
3422
  }
3430
- //#endregion
3431
- //#region lib/components/progress-bar/IxProgressBar.ts
3423
+ // lib/components/progress-bar/IxProgressBar.ts
3432
3424
  /**
3433
3425
  * IxProgressBar — viser fremdrift i én sammenhengende prosess.
3434
3426
  *
3435
3427
  * DOM-generator: web componenten eier og genererer all indre HTML (header med
3436
- * label + verdi/statusikon, track + fill, og en støttetekst-region).
3428
+ * label + verdi, track + fill, og en støttetekst-region).
3437
3429
  * Forfatteren skriver kun host-elementet `<ix-progress-bar>` med attributter —
3438
3430
  * ingen intern struktur.
3439
3431
  *
@@ -3441,9 +3433,11 @@ function isIOS() {
3441
3433
  * - `active` (default): role="progressbar" med aria-valuenow/min/max på host.
3442
3434
  * Nøytral/primær fremdriftsfarge. Ingen statusikon. Kan vise prosent.
3443
3435
  * - `success` / `error`: rollen `progressbar` FJERNES (prosessen er ferdig/avbrutt).
3444
- * Status formidles semantisk «tilsvarende Message» — `data-status`
3445
- * (success/danger) kobler --ix-color-status-* og et dekorativt
3446
- * `<ix-icon data-badge>` vises alltid. Støtteteksten er en aria-live-region
3436
+ * `data-status` (success/danger) kobler --ix-color-status-* til fyllet og
3437
+ * til støtteteksten, som får badge-ikon foran og ser ut som en
3438
+ * ValidationMessage, slik Figma viser. Den bruker ikke `data-field="error"`:
3439
+ * ix-field og gruppene leter etter det i hele subtreet og ville tatt
3440
+ * meldingen for feltets feilmelding. Støtteteksten er en aria-live-region
3447
3441
  * (role="status") så overgangen annonseres politely.
3448
3442
  *
3449
3443
  * Komponenten er rent informativ og aldri fokuserbar (setter aldri tabindex).
@@ -3457,7 +3451,7 @@ function isIOS() {
3457
3451
  var progressBarCounter = 0;
3458
3452
  var STATE_ICON = {
3459
3453
  success: "check",
3460
- error: "priority_high"
3454
+ error: "close"
3461
3455
  };
3462
3456
  var STATE_STATUS = {
3463
3457
  success: "success",
@@ -3535,8 +3529,9 @@ var IxProgressBar = class extends HTMLElement {
3535
3529
  this.style.setProperty("--ii-progress-bar-fill", `${fillPercent}%`);
3536
3530
  this._label.textContent = label ?? "";
3537
3531
  this._label.hidden = !label;
3538
- this._updateIconAndValue(state, clamped, showValue, isActive, Boolean(label));
3532
+ this._updateValue(clamped, showValue, isActive, Boolean(label));
3539
3533
  this._supportText.textContent = supportText ?? "";
3534
+ this._updateStatusMessage(state);
3540
3535
  Boolean(label) || this.hasAttribute("aria-label") || this.hasAttribute("aria-labelledby");
3541
3536
  if (isActive) {
3542
3537
  this.removeAttribute("data-status");
@@ -3572,32 +3567,28 @@ var IxProgressBar = class extends HTMLElement {
3572
3567
  this.setAttribute("data-status", STATE_STATUS[state]);
3573
3568
  }
3574
3569
  }
3570
+ /** Prosentverdien vises i header-slutten, bare i active. */
3571
+ _updateValue(clamped, showValue, isActive, hasLabel) {
3572
+ if (!this._headerEnd || !this._header) return;
3573
+ const visible = isActive && showValue;
3574
+ this._headerEnd.textContent = visible ? `${Math.round(clamped)}\u00A0%` : "";
3575
+ this._headerEnd.hidden = !visible;
3576
+ this._header.hidden = !hasLabel && !visible;
3577
+ }
3575
3578
  /**
3576
- * Plasserer statusikon og verdi. Følger Figma:
3577
- * - active + data-show-value → «NN %» i header-slutten.
3578
- * - success/error med label → ikon i header-slutten (til høyre for label).
3579
- * - success/error uten label → ikon foran støtteteksten.
3579
+ * I success/error får støtteteksten badge-ikon foran. Ikonet vises også
3580
+ * uten tekst, så tilstanden ikke bare vises med farge (WCAG 1.4.1).
3580
3581
  */
3581
- _updateIconAndValue(state, clamped, showValue, isActive, hasLabel) {
3582
- if (!this._headerEnd || !this._support || !this._supportText || !this._header) return;
3583
- if (isActive && showValue) {
3584
- this._removeIcon();
3585
- this._headerEnd.textContent = `${Math.round(clamped)}\u00A0%`;
3586
- this._headerEnd.hidden = false;
3587
- } else {
3588
- this._headerEnd.textContent = "";
3589
- this._headerEnd.hidden = true;
3590
- }
3591
- if (!isActive) {
3592
- const icon = this._ensureIcon();
3593
- icon.setAttribute("name", STATE_ICON[state]);
3594
- if (hasLabel) {
3595
- this._headerEnd.hidden = true;
3596
- this._header.appendChild(icon);
3597
- } else this._support.insertBefore(icon, this._supportText);
3598
- } else this._removeIcon();
3599
- const headerHasContent = hasLabel || isActive && showValue || !isActive && hasLabel;
3600
- this._header.hidden = !headerHasContent;
3582
+ _updateStatusMessage(state) {
3583
+ if (!this._support || !this._supportText) return;
3584
+ if (state === "active") {
3585
+ this._icon?.remove();
3586
+ return;
3587
+ }
3588
+ const icon = this._ensureIcon();
3589
+ icon.setAttribute("name", STATE_ICON[state]);
3590
+ icon.setAttribute("data-status", STATE_STATUS[state]);
3591
+ this._support.insertBefore(icon, this._supportText);
3601
3592
  }
3602
3593
  _ensureIcon() {
3603
3594
  if (!this._icon) {
@@ -3607,9 +3598,6 @@ var IxProgressBar = class extends HTMLElement {
3607
3598
  }
3608
3599
  return this._icon;
3609
3600
  }
3610
- _removeIcon() {
3611
- this._icon?.remove();
3612
- }
3613
3601
  _resolveState() {
3614
3602
  const raw = this.getAttribute("data-state");
3615
3603
  if (raw === "success" || raw === "error") return raw;
@@ -3628,64 +3616,32 @@ var IxProgressBar = class extends HTMLElement {
3628
3616
  else this.removeAttribute(attr);
3629
3617
  }
3630
3618
  };
3631
- //#endregion
3632
- //#region lib/components/radio-group/IxRadioGroup.ts
3619
+ // lib/components/radio-group/IxRadioGroup.ts
3633
3620
  /**
3634
- * IxRadioGroup — ARIA-lim for radioknapp-grupper.
3635
- *
3636
- * ## Designfilosofi
3637
- *
3638
- * Komponenten erstatter ingen native HTML-elementer. Forfatteren skriver
3639
- * native <input type="radio">, <label> og hjelpetekster inni <ix-radio-group>.
3640
- * Komponenten sin eneste jobb er å sette opp ARIA-koblingene mellom dem.
3641
- *
3642
- * ## Hva komponenten gjør
3643
- *
3644
- * 1. Setter role="radiogroup" på host slik at skjermlesere annonserer gruppen
3645
- * med riktig rolle og leser legend-teksten som gruppens tilgjengelige navn.
3646
- *
3647
- * 2. Kobler legend via aria-labelledby — genererer ID på [data-field="legend"]
3648
- * om nødvendig og peker host til den.
3649
- *
3650
- * 3. Setter aria-describedby på host med IDer til description og error
3651
- * (description alltid først). Hopper over elementer som mangler.
3652
- *
3653
- * 4. Genererer stabile IDer på hvert input og setter htmlFor på matchende label
3654
- * om label ikke allerede har det. Respekterer alltid eksisterende IDer.
3621
+ * IxRadioGroup kobler ARIA mellom native radioknapper, legend og hjelpetekster.
3622
+ * Forfatteren skriver markupen selv.
3655
3623
  *
3656
- * 5. Synkroniserer name-attributt på alle inputs — name er mekanismen
3657
- * nettleseren bruker for mutual exclusivity (ett valg av gangen) og
3658
- * ArrowKey-navigasjon mellom alternativene. Alle inputs i gruppen må ha
3659
- * samme name. Prioritet: name på host > name på input > auto-generert.
3660
- *
3661
- * 6. Setter aria-live="polite" på [data-field="error"] og observerer
3662
- * textContent-endringer: ikke-tomt innhold → aria-invalid="true" på host;
3663
- * tomt → aria-invalid fjernes. aria-invalid settes kun på host (ikke per
3664
- * input) — det er gruppens validitet som telles, og en duplisert flagg
3665
- * per input gir ingen merverdi for skjermlesere.
3666
- *
3667
- * 7. Propagerer disabled/readonly til alle inputs via attributeChangedCallback.
3668
- * Per-knapp disabled bevares — settes med en WeakMap ved første _wire().
3669
- *
3670
- * 8. Lytter på childList på host slik at nye <input type="radio">-elementer
3671
- * som legges til etter mount automatisk wires (id, name, htmlFor, disabled).
3672
- *
3673
- * ## Ingen Shadow DOM
3674
- *
3675
- * Komponenten bruker light DOM slik at CSS fra @sb1/indeks-css når alle
3676
- * delelementer direkte.
3624
+ * - role="radiogroup" og aria-labelledby til [data-field="legend"]
3625
+ * - aria-describedby til [data-field="description"] og [data-field="error"]
3626
+ * - id på hver input og for på labelen i samme forelder, når de mangler
3627
+ * - samme name på alle inputs: name på host, ellers name fra en input, ellers generert
3628
+ * - aria-invalid="true" på host (ikke per input) når error-feltet har tekst
3629
+ * - disabled, readonly og required på host gjelder hele gruppen
3630
+ * - inputs som legges til etter mount, kobles på samme måte
3677
3631
  *
3678
3632
  * @example
3679
3633
  * <ix-radio-group>
3680
3634
  * <span data-field="legend">Velg kundetype</span>
3681
3635
  * <span data-field="description">Beskrivelse</span>
3682
- * <div class="ix-radio-button">
3683
- * <input type="radio" value="privat" />
3684
- * <label>Privat</label>
3685
- * </div>
3686
- * <div class="ix-radio-button">
3687
- * <input type="radio" value="bedrift" />
3688
- * <label>Bedrift</label>
3636
+ * <div data-field="items">
3637
+ * <div>
3638
+ * <input type="radio" value="privat" />
3639
+ * <label>Privat</label>
3640
+ * </div>
3641
+ * <div>
3642
+ * <input type="radio" value="bedrift" />
3643
+ * <label>Bedrift</label>
3644
+ * </div>
3689
3645
  * </div>
3690
3646
  * <span data-field="error"></span>
3691
3647
  * </ix-radio-group>
@@ -3751,7 +3707,8 @@ var IxRadioGroup = class extends HTMLElement {
3751
3707
  this._errorObserver.observe(error, {
3752
3708
  childList: true,
3753
3709
  characterData: true,
3754
- subtree: true
3710
+ subtree: true,
3711
+ attributeFilter: ["data-status"]
3755
3712
  });
3756
3713
  this._syncInvalid(error);
3757
3714
  }
@@ -3794,7 +3751,7 @@ var IxRadioGroup = class extends HTMLElement {
3794
3751
  if (inputs.length === 0) return;
3795
3752
  for (const input of inputs) {
3796
3753
  if (!input.id) input.id = `ix-radio-${++radioInputCounter}`;
3797
- const wrapper = input.closest(".ix-radio-button") ?? input.parentElement;
3754
+ const wrapper = input.parentElement;
3798
3755
  if (wrapper) {
3799
3756
  const label = wrapper.querySelector("label");
3800
3757
  if (label && !label.htmlFor) label.htmlFor = input.id;
@@ -3807,7 +3764,7 @@ var IxRadioGroup = class extends HTMLElement {
3807
3764
  this._syncHostStateToInputs();
3808
3765
  }
3809
3766
  _syncInvalid(error) {
3810
- if (!!error.textContent?.trim()) this.setAttribute("aria-invalid", "true");
3767
+ if (!!error.textContent?.trim() && error.dataset.status !== "success") this.setAttribute("aria-invalid", "true");
3811
3768
  else this.removeAttribute("aria-invalid");
3812
3769
  }
3813
3770
  _syncHostStateToInputs() {
@@ -3833,69 +3790,30 @@ var IxRadioGroup = class extends HTMLElement {
3833
3790
  }
3834
3791
  }
3835
3792
  };
3836
- //#endregion
3837
- //#region lib/components/tabs/IxTabs.ts
3793
+ // lib/components/tabs/IxTabs.ts
3838
3794
  /**
3839
- * IxTabs — ARIA-lim for faner (tabs) i light DOM.
3840
- *
3841
- * ## Designfilosofi
3842
- *
3843
- * Komponenten erstatter ingen native elementer. Forfatteren skriver semantisk
3844
- * HTML — en liste med faner og ett panel per fane — og <ix-tabs> kobler dem
3845
- * sammen med ARIA, roving tabindex, tastaturnavigasjon og panel-visning.
3846
- * Logikken er portet fra u-elements (u-tabs), men til Indeks' light-DOM-
3847
- * konvensjon (ingen Shadow DOM, ingen ::slotted).
3848
- *
3849
- * Fire custom elements samarbeider:
3850
- * - <ix-tabs> — rot og koordinator. Eier all kabling og tastatur.
3851
- * - <ix-tab-list> — role="tablist", horisontal orientering.
3852
- * - <ix-tab> — role="tab", én per fane.
3853
- * - <ix-tab-panel> — role="tabpanel", ett innholdspanel per fane.
3854
- *
3855
- * Underelementene setter kun sin egen `role` (så de fungerer isolert); <ix-tabs>
3856
- * er den autoritative koordinatoren og kjører ved connectedCallback.
3857
- *
3858
- * ## Hva <ix-tabs> gjør
3859
- *
3860
- * 1. Setter role="tablist" + aria-orientation="horizontal" på tab-lista,
3861
- * role="tab" på fanene og role="tabpanel" på panelene.
3862
- * 2. Genererer stabile IDer på faner og paneler (der de mangler) og setter opp
3863
- * kryssreferansene aria-controls (fane → panel) og aria-labelledby
3864
- * (panel → fane). Eksisterende IDer og aria-controls respekteres. Fane[i]
3865
- * kobles til panel[i], eller til panelet fanens aria-controls peker på.
3866
- * 3. Manuell aktivering: piltast (Venstre/Høyre) og Home/End flytter kun FOKUS
3867
- * via roving tabindex (fokusert fane får tabindex=0, resten -1).
3868
- * Piltast-navigasjonen looper rundt endene. Enter/Space (og klikk) aktiverer
3869
- * den fokuserte fanen.
3870
- * 4. Kun én fane er valgt om gangen. Ved aktivering flyttes aria-selected +
3871
- * tabindex til ny fane, tilhørende panel vises, øvrige skjules ([hidden] +
3872
- * aria-hidden). Panelet gjøres ikke fokuserbart — det er ikke en egen
3873
- * tab-stopp; innholdet i panelet bærer sin egen fokusrekkefølge.
3874
- * Fane→panel-kobling skjer via delt data-value (ellers aria-controls,
3875
- * ellers posisjon) — se _getPanel.
3876
- * 5. Minst én fane er valgt ved mount — enten den som har aria-selected="true"
3877
- * i markup, ellers den første fanen.
3878
- * 6. Ved aktivering (klikk/tastatur) sendes et bubbling CustomEvent('change')
3879
- * på <ix-tabs> slik at en React-wrapper kan lese aria-selected og kalle
3880
- * onChange. Programmatisk init og ekstern (kontrollert) synk sender IKKE
3881
- * change — kun ekte brukerinteraksjon.
3882
- * 7. En MutationObserver re-wirer når faner/paneler legges til eller fjernes
3883
- * (typisk React conditional rendering), og reagerer på ekstern endring av
3884
- * aria-selected (kontrollert React-modus) ved å reconcile-e resten.
3885
- *
3886
- * ## Ingen Shadow DOM
3795
+ * IxTabs — ARIA-lim for faner i light DOM. Forfatteren skriver en liste med
3796
+ * faner og ett panel per fane; <ix-tabs> kobler dem med ARIA, roving tabindex,
3797
+ * tastaturnavigasjon og panel-visning. Portet fra u-elements (u-tabs), men uten
3798
+ * Shadow DOM, slik at CSS treffer custom elementene og ARIA-relasjonene virker
3799
+ * på tvers.
3887
3800
  *
3888
- * Light DOM slik at CSS fra @sb1/indeks-css treffer .ix-tabs__* og tag-
3889
- * selektorene direkte, og ARIA-relasjonene fungerer på tvers av elementene.
3801
+ * - Manuell aktivering: piltaster og Home/End flytter bare fokus; Enter/Space
3802
+ * og klikk aktiverer. Panelet er ikke en egen tab-stopp.
3803
+ * - Fane og panel kobles via aria-controls, ellers delt data-value, ellers
3804
+ * posisjon (se _getPanel).
3805
+ * - Ved klikk/tastatur sendes et bubbling `change` på <ix-tabs>. Init og
3806
+ * ekstern (kontrollert) synk sender ikke change.
3807
+ * - Underelementene setter bare sin egen `role`; <ix-tabs> er koordinatoren.
3890
3808
  *
3891
3809
  * @example
3892
3810
  * <ix-tabs class="ix-tabs">
3893
- * <ix-tab-list class="ix-tabs__list">
3894
- * <ix-tab class="ix-tabs__tab">Oversikt</ix-tab>
3895
- * <ix-tab class="ix-tabs__tab">Detaljer</ix-tab>
3811
+ * <ix-tab-list>
3812
+ * <ix-tab>Oversikt</ix-tab>
3813
+ * <ix-tab>Detaljer</ix-tab>
3896
3814
  * </ix-tab-list>
3897
- * <ix-tab-panel class="ix-tabs__panel">Oversikt-innhold</ix-tab-panel>
3898
- * <ix-tab-panel class="ix-tabs__panel">Detaljer-innhold</ix-tab-panel>
3815
+ * <ix-tab-panel>Oversikt-innhold</ix-tab-panel>
3816
+ * <ix-tab-panel>Detaljer-innhold</ix-tab-panel>
3899
3817
  * </ix-tabs>
3900
3818
  */
3901
3819
  var tabsCounter = 0;
@@ -4104,8 +4022,7 @@ var IxTabPanel = class extends HTMLElement {
4104
4022
  if (!this.getAttribute(ROLE)) this.setAttribute(ROLE, "tabpanel");
4105
4023
  }
4106
4024
  };
4107
- //#endregion
4108
- //#region lib/utils/cn.ts
4025
+ // lib/utils/cn.ts
4109
4026
  function cn(...inputs) {
4110
4027
  const klasser = [];
4111
4028
  for (const input of inputs) {
@@ -4120,8 +4037,7 @@ function cn(...inputs) {
4120
4037
  }
4121
4038
  return klasser.join(" ");
4122
4039
  }
4123
- //#endregion
4124
- //#region lib/modal/modal.ts
4040
+ // lib/modal/modal.ts
4125
4041
  var ATTR_OPEN = "data-modal-open";
4126
4042
  var ATTR_CLOSE = "data-modal-close";
4127
4043
  var ATTR_NO_BACKDROP = "data-no-close-on-backdrop";
@@ -4174,22 +4090,24 @@ function handleClose(e) {
4174
4090
  }
4175
4091
  var observer = new MutationObserver((records) => {
4176
4092
  for (const record of records) {
4093
+ if (record.type !== "attributes") continue;
4177
4094
  const dialog = record.target;
4178
4095
  if (!(dialog instanceof HTMLDialogElement)) continue;
4179
4096
  if (!dialog.classList.contains("ix-modal")) continue;
4180
4097
  if (dialog.open) handleOpened(dialog);
4181
4098
  else handleClosed(dialog);
4182
4099
  }
4100
+ for (const dialog of openModals) if (!dialog.isConnected) handleClosed(dialog);
4183
4101
  });
4184
4102
  document.addEventListener("click", handleClick);
4185
4103
  document.addEventListener("close", handleClose, true);
4186
4104
  observer.observe(document.documentElement, {
4187
4105
  subtree: true,
4106
+ childList: true,
4188
4107
  attributes: true,
4189
4108
  attributeFilter: ["open"]
4190
4109
  });
4191
- //#endregion
4192
- //#region lib/tooltip/tooltip.ts
4110
+ // lib/tooltip/tooltip.ts
4193
4111
  var TOOLTIP_ID = "ix-tooltip-singleton";
4194
4112
  var ATTR = "data-tooltip";
4195
4113
  var ATTR_PLACEMENT = "data-tooltip-placement";
@@ -4306,8 +4224,7 @@ document.addEventListener("mouseout", handleMouseout);
4306
4224
  document.addEventListener("focusin", handleFocusin);
4307
4225
  document.addEventListener("focusout", handleFocusout);
4308
4226
  document.addEventListener("keydown", handleKeydown);
4309
- //#endregion
4310
- //#region index.ts
4227
+ // index.ts
4311
4228
  customElements.define("ix-field", IxField);
4312
4229
  customElements.define("ix-icon", IxIcon);
4313
4230
  customElements.define("ix-radio-group", IxRadioGroup);
@@ -4323,5 +4240,4 @@ customElements.define("ix-tab-list", IxTabList);
4323
4240
  customElements.define("ix-tab", IxTab);
4324
4241
  customElements.define("ix-tab-panel", IxTabPanel);
4325
4242
  globalThis.cn = cn;
4326
- //#endregion
4327
4243
  export { BUILTIN_FORMAT_NAMES, COMMON_ICON_NAMES, IxCheckboxGroup, IxCombobox, IxDateField, IxDropdown, IxField, IxIcon, IxPhoneNumberField, IxPopover, IxProgressBar, IxRadioGroup, IxTab, IxTabList, IxTabPanel, IxTabs, amountFormatterForLocale, cn, createAmountFormatter, createPatternFormatter, registerFormat, resolveFormat };
@@ -43,7 +43,7 @@
43
43
  * @example Single
44
44
  * <ix-combobox data-no-hits-text="Ingen treff">
45
45
  * <div class="ix-text-field">
46
- * <input class="ix-text-field__input" placeholder="Søk …" />
46
+ * <input placeholder="Søk …" />
47
47
  * <button class="ix-combobox__toggle"></button>
48
48
  * </div>
49
49
  * <div class="ix-combobox__listbox">
@@ -16,6 +16,7 @@ export declare class IxPhoneNumberField extends HTMLElement {
16
16
  private _containsGroupField;
17
17
  private _combobox;
18
18
  private _numberInput;
19
+ private _hasError;
19
20
  private _syncInvalid;
20
21
  private _syncDataState;
21
22
  private _syncHostStateToControls;
@@ -2,7 +2,7 @@
2
2
  * IxProgressBar — viser fremdrift i én sammenhengende prosess.
3
3
  *
4
4
  * DOM-generator: web componenten eier og genererer all indre HTML (header med
5
- * label + verdi/statusikon, track + fill, og en støttetekst-region).
5
+ * label + verdi, track + fill, og en støttetekst-region).
6
6
  * Forfatteren skriver kun host-elementet `<ix-progress-bar>` med attributter —
7
7
  * ingen intern struktur.
8
8
  *
@@ -10,9 +10,11 @@
10
10
  * - `active` (default): role="progressbar" med aria-valuenow/min/max på host.
11
11
  * Nøytral/primær fremdriftsfarge. Ingen statusikon. Kan vise prosent.
12
12
  * - `success` / `error`: rollen `progressbar` FJERNES (prosessen er ferdig/avbrutt).
13
- * Status formidles semantisk «tilsvarende Message» — `data-status`
14
- * (success/danger) kobler --ix-color-status-* og et dekorativt
15
- * `<ix-icon data-badge>` vises alltid. Støtteteksten er en aria-live-region
13
+ * `data-status` (success/danger) kobler --ix-color-status-* til fyllet og
14
+ * til støtteteksten, som får badge-ikon foran og ser ut som en
15
+ * ValidationMessage, slik Figma viser. Den bruker ikke `data-field="error"`:
16
+ * ix-field og gruppene leter etter det i hele subtreet og ville tatt
17
+ * meldingen for feltets feilmelding. Støtteteksten er en aria-live-region
16
18
  * (role="status") så overgangen annonseres politely.
17
19
  *
18
20
  * Komponenten er rent informativ og aldri fokuserbar (setter aldri tabindex).
@@ -41,15 +43,14 @@ export declare class IxProgressBar extends HTMLElement {
41
43
  /** Bygger den indre strukturen én gang (idempotent). */
42
44
  private _ensureStructure;
43
45
  private _update;
46
+ /** Prosentverdien vises i header-slutten, bare i active. */
47
+ private _updateValue;
44
48
  /**
45
- * Plasserer statusikon og verdi. Følger Figma:
46
- * - active + data-show-value → «NN %» i header-slutten.
47
- * - success/error med label → ikon i header-slutten (til høyre for label).
48
- * - success/error uten label → ikon foran støtteteksten.
49
+ * I success/error får støtteteksten badge-ikon foran. Ikonet vises også
50
+ * uten tekst, så tilstanden ikke bare vises med farge (WCAG 1.4.1).
49
51
  */
50
- private _updateIconAndValue;
52
+ private _updateStatusMessage;
51
53
  private _ensureIcon;
52
- private _removeIcon;
53
54
  private _resolveState;
54
55
  /** Klamper til 0–100. Ikke-numerisk → 0. */
55
56
  private _clampValue;
@@ -1,59 +1,28 @@
1
1
  /**
2
- * IxRadioGroup — ARIA-lim for radioknapp-grupper.
2
+ * IxRadioGroup kobler ARIA mellom native radioknapper, legend og hjelpetekster.
3
+ * Forfatteren skriver markupen selv.
3
4
  *
4
- * ## Designfilosofi
5
- *
6
- * Komponenten erstatter ingen native HTML-elementer. Forfatteren skriver
7
- * native <input type="radio">, <label> og hjelpetekster inni <ix-radio-group>.
8
- * Komponenten sin eneste jobb er å sette opp ARIA-koblingene mellom dem.
9
- *
10
- * ## Hva komponenten gjør
11
- *
12
- * 1. Setter role="radiogroup" på host slik at skjermlesere annonserer gruppen
13
- * med riktig rolle og leser legend-teksten som gruppens tilgjengelige navn.
14
- *
15
- * 2. Kobler legend via aria-labelledby — genererer ID på [data-field="legend"]
16
- * om nødvendig og peker host til den.
17
- *
18
- * 3. Setter aria-describedby på host med IDer til description og error
19
- * (description alltid først). Hopper over elementer som mangler.
20
- *
21
- * 4. Genererer stabile IDer på hvert input og setter htmlFor på matchende label
22
- * om label ikke allerede har det. Respekterer alltid eksisterende IDer.
23
- *
24
- * 5. Synkroniserer name-attributt på alle inputs — name er mekanismen
25
- * nettleseren bruker for mutual exclusivity (ett valg av gangen) og
26
- * ArrowKey-navigasjon mellom alternativene. Alle inputs i gruppen må ha
27
- * samme name. Prioritet: name på host > name på input > auto-generert.
28
- *
29
- * 6. Setter aria-live="polite" på [data-field="error"] og observerer
30
- * textContent-endringer: ikke-tomt innhold → aria-invalid="true" på host;
31
- * tomt → aria-invalid fjernes. aria-invalid settes kun på host (ikke per
32
- * input) — det er gruppens validitet som telles, og en duplisert flagg
33
- * per input gir ingen merverdi for skjermlesere.
34
- *
35
- * 7. Propagerer disabled/readonly til alle inputs via attributeChangedCallback.
36
- * Per-knapp disabled bevares — settes med en WeakMap ved første _wire().
37
- *
38
- * 8. Lytter på childList på host slik at nye <input type="radio">-elementer
39
- * som legges til etter mount automatisk wires (id, name, htmlFor, disabled).
40
- *
41
- * ## Ingen Shadow DOM
42
- *
43
- * Komponenten bruker light DOM slik at CSS fra @sb1/indeks-css når alle
44
- * delelementer direkte.
5
+ * - role="radiogroup" og aria-labelledby til [data-field="legend"]
6
+ * - aria-describedby til [data-field="description"] og [data-field="error"]
7
+ * - id på hver input og for på labelen i samme forelder, når de mangler
8
+ * - samme name på alle inputs: name på host, ellers name fra en input, ellers generert
9
+ * - aria-invalid="true" på host (ikke per input) når error-feltet har tekst
10
+ * - disabled, readonly og required på host gjelder hele gruppen
11
+ * - inputs som legges til etter mount, kobles på samme måte
45
12
  *
46
13
  * @example
47
14
  * <ix-radio-group>
48
15
  * <span data-field="legend">Velg kundetype</span>
49
16
  * <span data-field="description">Beskrivelse</span>
50
- * <div class="ix-radio-button">
51
- * <input type="radio" value="privat" />
52
- * <label>Privat</label>
53
- * </div>
54
- * <div class="ix-radio-button">
55
- * <input type="radio" value="bedrift" />
56
- * <label>Bedrift</label>
17
+ * <div data-field="items">
18
+ * <div>
19
+ * <input type="radio" value="privat" />
20
+ * <label>Privat</label>
21
+ * </div>
22
+ * <div>
23
+ * <input type="radio" value="bedrift" />
24
+ * <label>Bedrift</label>
25
+ * </div>
57
26
  * </div>
58
27
  * <span data-field="error"></span>
59
28
  * </ix-radio-group>
@@ -1,64 +1,26 @@
1
1
  /**
2
- * IxTabs — ARIA-lim for faner (tabs) i light DOM.
2
+ * IxTabs — ARIA-lim for faner i light DOM. Forfatteren skriver en liste med
3
+ * faner og ett panel per fane; <ix-tabs> kobler dem med ARIA, roving tabindex,
4
+ * tastaturnavigasjon og panel-visning. Portet fra u-elements (u-tabs), men uten
5
+ * Shadow DOM, slik at CSS treffer custom elementene og ARIA-relasjonene virker
6
+ * på tvers.
3
7
  *
4
- * ## Designfilosofi
5
- *
6
- * Komponenten erstatter ingen native elementer. Forfatteren skriver semantisk
7
- * HTML — en liste med faner og ett panel per fane — og <ix-tabs> kobler dem
8
- * sammen med ARIA, roving tabindex, tastaturnavigasjon og panel-visning.
9
- * Logikken er portet fra u-elements (u-tabs), men til Indeks' light-DOM-
10
- * konvensjon (ingen Shadow DOM, ingen ::slotted).
11
- *
12
- * Fire custom elements samarbeider:
13
- * - <ix-tabs> — rot og koordinator. Eier all kabling og tastatur.
14
- * - <ix-tab-list> — role="tablist", horisontal orientering.
15
- * - <ix-tab> — role="tab", én per fane.
16
- * - <ix-tab-panel> — role="tabpanel", ett innholdspanel per fane.
17
- *
18
- * Underelementene setter kun sin egen `role` (så de fungerer isolert); <ix-tabs>
19
- * er den autoritative koordinatoren og kjører ved connectedCallback.
20
- *
21
- * ## Hva <ix-tabs> gjør
22
- *
23
- * 1. Setter role="tablist" + aria-orientation="horizontal" på tab-lista,
24
- * role="tab" på fanene og role="tabpanel" på panelene.
25
- * 2. Genererer stabile IDer på faner og paneler (der de mangler) og setter opp
26
- * kryssreferansene aria-controls (fane → panel) og aria-labelledby
27
- * (panel → fane). Eksisterende IDer og aria-controls respekteres. Fane[i]
28
- * kobles til panel[i], eller til panelet fanens aria-controls peker på.
29
- * 3. Manuell aktivering: piltast (Venstre/Høyre) og Home/End flytter kun FOKUS
30
- * via roving tabindex (fokusert fane får tabindex=0, resten -1).
31
- * Piltast-navigasjonen looper rundt endene. Enter/Space (og klikk) aktiverer
32
- * den fokuserte fanen.
33
- * 4. Kun én fane er valgt om gangen. Ved aktivering flyttes aria-selected +
34
- * tabindex til ny fane, tilhørende panel vises, øvrige skjules ([hidden] +
35
- * aria-hidden). Panelet gjøres ikke fokuserbart — det er ikke en egen
36
- * tab-stopp; innholdet i panelet bærer sin egen fokusrekkefølge.
37
- * Fane→panel-kobling skjer via delt data-value (ellers aria-controls,
38
- * ellers posisjon) — se _getPanel.
39
- * 5. Minst én fane er valgt ved mount — enten den som har aria-selected="true"
40
- * i markup, ellers den første fanen.
41
- * 6. Ved aktivering (klikk/tastatur) sendes et bubbling CustomEvent('change')
42
- * på <ix-tabs> slik at en React-wrapper kan lese aria-selected og kalle
43
- * onChange. Programmatisk init og ekstern (kontrollert) synk sender IKKE
44
- * change — kun ekte brukerinteraksjon.
45
- * 7. En MutationObserver re-wirer når faner/paneler legges til eller fjernes
46
- * (typisk React conditional rendering), og reagerer på ekstern endring av
47
- * aria-selected (kontrollert React-modus) ved å reconcile-e resten.
48
- *
49
- * ## Ingen Shadow DOM
50
- *
51
- * Light DOM slik at CSS fra @sb1/indeks-css treffer .ix-tabs__* og tag-
52
- * selektorene direkte, og ARIA-relasjonene fungerer på tvers av elementene.
8
+ * - Manuell aktivering: piltaster og Home/End flytter bare fokus; Enter/Space
9
+ * og klikk aktiverer. Panelet er ikke en egen tab-stopp.
10
+ * - Fane og panel kobles via aria-controls, ellers delt data-value, ellers
11
+ * posisjon (se _getPanel).
12
+ * - Ved klikk/tastatur sendes et bubbling `change` på <ix-tabs>. Init og
13
+ * ekstern (kontrollert) synk sender ikke change.
14
+ * - Underelementene setter bare sin egen `role`; <ix-tabs> er koordinatoren.
53
15
  *
54
16
  * @example
55
17
  * <ix-tabs class="ix-tabs">
56
- * <ix-tab-list class="ix-tabs__list">
57
- * <ix-tab class="ix-tabs__tab">Oversikt</ix-tab>
58
- * <ix-tab class="ix-tabs__tab">Detaljer</ix-tab>
18
+ * <ix-tab-list>
19
+ * <ix-tab>Oversikt</ix-tab>
20
+ * <ix-tab>Detaljer</ix-tab>
59
21
  * </ix-tab-list>
60
- * <ix-tab-panel class="ix-tabs__panel">Oversikt-innhold</ix-tab-panel>
61
- * <ix-tab-panel class="ix-tabs__panel">Detaljer-innhold</ix-tab-panel>
22
+ * <ix-tab-panel>Oversikt-innhold</ix-tab-panel>
23
+ * <ix-tab-panel>Detaljer-innhold</ix-tab-panel>
62
24
  * </ix-tabs>
63
25
  */
64
26
  export declare class IxTabs extends HTMLElement {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sb1/indeks-web",
3
- "version": "0.24.0",
3
+ "version": "0.26.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": "^26.6.2",
44
- "jsdom": "^30.1.0",
43
+ "@types/node": "^26.6.5",
44
+ "jsdom": "^30.1.3",
45
45
  "typescript": "~6.0.3",
46
- "vite": "^8.2.2",
47
- "vite-plugin-dts": "^5.1.1",
48
- "vitest": "^4.1.10"
46
+ "vite": "^8.3.5",
47
+ "vite-plugin-dts": "^5.1.2",
48
+ "vitest": "^5.0.5"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "rm -rf ./dist && pnpm run build:npm",