@sb1/indeks-web 0.19.0 → 0.20.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,22 @@
1
1
  # @sb1/indeks-web
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a64a806: Ny Popover-komponent for kontekstuell informasjon og handlinger
8
+
9
+ - CSS: `.ix-popover`, `.ix-popover__content`, `.ix-popover__heading`, `.ix-popover__body`, `.ix-popover__actions`
10
+ - Web component: `<ix-popover>` med ARIA-lim, posisjonering og fokushåndtering
11
+ - React: `<Popover>` med sub-komponenter `Popover.Trigger`, `Popover.Content`, `Popover.Heading`, `Popover.Body`, `Popover.Actions`
12
+
13
+ ### Patch Changes
14
+
15
+ - b71ecc3: Fiks heading i Popover og åpning av submenyer på hover i DropdownMenu
16
+
17
+ - Popover: Heading er nå stylet som en overskrift
18
+ - DropdownMenu: Submenyer åpnes nå ved hover i tillegg til tastatur (ArrowRight)
19
+
3
20
  ## 0.19.0
4
21
 
5
22
  ### Minor Changes
@@ -5,6 +5,7 @@ import { IxDropdown } from './lib/components/dropdown/IxDropdown.js';
5
5
  import { IxField } from './lib/components/field/IxField.js';
6
6
  import { IxIcon, IconName } from './lib/components/icon/IxIcon.js';
7
7
  import { IxPhoneNumberField } from './lib/components/phone-number-field/IxPhoneNumberField.js';
8
+ import { IxPopover } from './lib/components/popover/IxPopover.js';
8
9
  import { IxProgressBar } from './lib/components/progress-bar/IxProgressBar.js';
9
10
  import { IxRadioGroup } from './lib/components/radio-group/IxRadioGroup.js';
10
11
  import { IxTabs, IxTabList, IxTab, IxTabPanel } from './lib/components/tabs/IxTabs.js';
@@ -16,6 +17,7 @@ export { IxCombobox };
16
17
  export { IxDateField };
17
18
  export { IxDropdown };
18
19
  export { IxPhoneNumberField };
20
+ export { IxPopover };
19
21
  export { IxProgressBar };
20
22
  export type { ProgressBarState } from './lib/components/progress-bar/IxProgressBar.js';
21
23
  export { IxTabs, IxTabList, IxTab, IxTabPanel };
@@ -36,6 +38,7 @@ declare global {
36
38
  'ix-date-field': IxDateField;
37
39
  'ix-dropdown': IxDropdown;
38
40
  'ix-phone-number-field': IxPhoneNumberField;
41
+ 'ix-popover': IxPopover;
39
42
  'ix-progress-bar': IxProgressBar;
40
43
  'ix-tabs': IxTabs;
41
44
  'ix-tab-list': IxTabList;
@@ -109,6 +112,14 @@ declare global {
109
112
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
110
113
  'data-submenu'?: boolean | '';
111
114
  };
115
+ 'ix-popover': {
116
+ open?: boolean | '';
117
+ placement?: 'top' | 'bottom' | 'left' | 'right';
118
+ 'data-arrow'?: 'true' | 'false';
119
+ 'data-controlled'?: boolean | '';
120
+ class?: string;
121
+ children?: unknown;
122
+ };
112
123
  'ix-icon': {
113
124
  name?: IconName;
114
125
  'data-size'?: 'sm' | 'md' | 'lg' | 'xl';
package/dist/npm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/typeof.js
1
+ //#region \0@oxc-project+runtime@0.144.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) {
@@ -8,7 +8,7 @@ function _typeof(o) {
8
8
  }, _typeof(o);
9
9
  }
10
10
  //#endregion
11
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPrimitive.js
11
+ //#region \0@oxc-project+runtime@0.144.0/helpers/esm/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.139.0/helpers/esm/toPropertyKey.js
23
+ //#region \0@oxc-project+runtime@0.144.0/helpers/esm/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.139.0/helpers/esm/defineProperty.js
29
+ //#region \0@oxc-project+runtime@0.144.0/helpers/esm/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,
@@ -620,16 +620,14 @@ var IxCombobox = class extends HTMLElement {
620
620
  }
621
621
  break;
622
622
  case "ArrowLeft":
623
- case "Backspace":
624
- if (this.multiple && input.selectionStart === 0 && input.selectionEnd === 0) {
625
- const chips = this._chipElements();
626
- const last = chips[chips.length - 1];
627
- if (last) {
628
- e.preventDefault();
629
- last.focus();
630
- }
623
+ case "Backspace": if (this.multiple && input.selectionStart === 0 && input.selectionEnd === 0) {
624
+ const chips = this._chipElements();
625
+ const last = chips[chips.length - 1];
626
+ if (last) {
627
+ e.preventDefault();
628
+ last.focus();
631
629
  }
632
- break;
630
+ }
633
631
  }
634
632
  }
635
633
  _handleChipKeydown(e, chip) {
@@ -1558,7 +1556,6 @@ var IxDropdown = class extends HTMLElement {
1558
1556
  this._openedWithKeyboard = true;
1559
1557
  this._open();
1560
1558
  this._focusItem(this._items.length - 1);
1561
- break;
1562
1559
  }
1563
1560
  }
1564
1561
  _handleMenuKeydown(e) {
@@ -1634,6 +1631,18 @@ var IxDropdown = class extends HTMLElement {
1634
1631
  _handleItemPointerEnter(index) {
1635
1632
  this._activeIndex = index;
1636
1633
  if (this._menu?.contains(document.activeElement)) this._focusItem(index);
1634
+ const item = this._items[index];
1635
+ if (!item) return;
1636
+ const submenu = item.parentElement;
1637
+ if (submenu?.hasAttribute("data-submenu") && item === submenu._trigger) {
1638
+ this._closeOtherSubmenus(submenu);
1639
+ submenu._open();
1640
+ } else this._closeAllSubmenus();
1641
+ }
1642
+ _closeOtherSubmenus(keep) {
1643
+ (this._menu?.querySelectorAll("ix-dropdown[data-submenu]"))?.forEach((submenu) => {
1644
+ if (submenu !== keep && submenu._isOpen) submenu._close();
1645
+ });
1637
1646
  }
1638
1647
  _handleItemClick(e, item) {
1639
1648
  if (item.hasAttribute("disabled") || item.getAttribute("aria-disabled") === "true") {
@@ -2980,9 +2989,10 @@ var IxPhoneNumberField = class extends HTMLElement {
2980
2989
  const prop = flag === "readonly" ? "readOnly" : flag;
2981
2990
  const read = () => mode === "attribute" ? control.hasAttribute(flag) : !!control[prop];
2982
2991
  const write = (value) => {
2983
- if (mode === "attribute") if (value) control.setAttribute(flag, "");
2984
- else control.removeAttribute(flag);
2985
- else control[prop] = value;
2992
+ if (mode === "attribute") {
2993
+ if (value) control.setAttribute(flag, "");
2994
+ else control.removeAttribute(flag);
2995
+ } else control[prop] = value;
2986
2996
  };
2987
2997
  if (groupOn) {
2988
2998
  if (!ownStore.has(control)) ownStore.set(control, read());
@@ -3084,6 +3094,317 @@ var IxPhoneNumberField = class extends HTMLElement {
3084
3094
  }
3085
3095
  };
3086
3096
  //#endregion
3097
+ //#region lib/components/popover/IxPopover.ts
3098
+ /**
3099
+ * IxPopover — ARIA-lim for en popover med kontekstuell informasjon eller handlinger.
3100
+ *
3101
+ * ## Designfilosofi
3102
+ *
3103
+ * Komponenten kobler en trigger-knapp med et innholdspanel. Forfatteren skriver
3104
+ * en <button> (trigger) og en .ix-popover__content i light DOM. Komponentens jobb
3105
+ * er å sette opp ARIA-attributter, håndtere åpne/lukke-logikk, posisjonere panelet,
3106
+ * og styre fokus.
3107
+ *
3108
+ * ## Fokushåndtering
3109
+ *
3110
+ * Når popoveren åpnes, flyttes fokus til første focusable element i content (hvis det
3111
+ * finnes). Når popoveren lukkes med Escape, returneres fokus til trigger. Fokus trappes IKKE —
3112
+ * brukeren kan tabbe ut av popoveren naturlig.
3113
+ *
3114
+ * ## Posisjonering
3115
+ *
3116
+ * Panelet posisjoneres manuelt med getBoundingClientRect + viewport-clamp
3117
+ * (samme grep som dropdown og tooltip) — position: fixed, JS setter top/left.
3118
+ * Auto-flip brukes hvis det ikke er nok plass i ønsket retning.
3119
+ *
3120
+ * @example
3121
+ * <ix-popover placement="top">
3122
+ * <button class="ix-button">Åpne popover</button>
3123
+ * <div class="ix-popover__content">
3124
+ * <div class="ix-popover__heading">Overskrift</div>
3125
+ * <div class="ix-popover__body">Forklarende tekst her.</div>
3126
+ * <div class="ix-popover__actions">
3127
+ * <button class="ix-button" data-size="sm">OK</button>
3128
+ * </div>
3129
+ * </div>
3130
+ * </ix-popover>
3131
+ */
3132
+ var popoverCounter = 0;
3133
+ var IxPopover = class extends HTMLElement {
3134
+ constructor(..._args) {
3135
+ super(..._args);
3136
+ _defineProperty(this, "_instanceId", 0);
3137
+ _defineProperty(this, "_trigger", null);
3138
+ _defineProperty(this, "_content", null);
3139
+ _defineProperty(this, "_onTriggerClick", null);
3140
+ _defineProperty(this, "_onTriggerKeydown", null);
3141
+ _defineProperty(this, "_onContentKeydown", null);
3142
+ _defineProperty(this, "_onDocPointer", null);
3143
+ _defineProperty(this, "_onFocusout", null);
3144
+ _defineProperty(this, "_onReposition", null);
3145
+ }
3146
+ static get observedAttributes() {
3147
+ return ["open", "placement"];
3148
+ }
3149
+ /**
3150
+ * Kontrollert modus: når `data-controlled` er satt, endrer ikke komponenten
3151
+ * egen åpen-tilstand. Den dispatcher kun `open-request` / `close-request`,
3152
+ * og foreldren må sette `open`-attributtet for å faktisk åpne/lukke.
3153
+ */
3154
+ get _isControlled() {
3155
+ return this.hasAttribute("data-controlled");
3156
+ }
3157
+ connectedCallback() {
3158
+ this._instanceId = ++popoverCounter;
3159
+ this._wire();
3160
+ }
3161
+ disconnectedCallback() {
3162
+ if (this._trigger) {
3163
+ if (this._onTriggerClick) this._trigger.removeEventListener("click", this._onTriggerClick);
3164
+ if (this._onTriggerKeydown) this._trigger.removeEventListener("keydown", this._onTriggerKeydown);
3165
+ }
3166
+ if (this._content) {
3167
+ if (this._onContentKeydown) this._content.removeEventListener("keydown", this._onContentKeydown);
3168
+ }
3169
+ if (this._onDocPointer) document.removeEventListener("pointerdown", this._onDocPointer, true);
3170
+ if (this._onFocusout) this.removeEventListener("focusout", this._onFocusout);
3171
+ this._removeRepositionListeners();
3172
+ this._onTriggerClick = null;
3173
+ this._onTriggerKeydown = null;
3174
+ this._onContentKeydown = null;
3175
+ this._onDocPointer = null;
3176
+ this._onFocusout = null;
3177
+ this._onReposition = null;
3178
+ this._trigger = null;
3179
+ this._content = null;
3180
+ }
3181
+ attributeChangedCallback(name, _oldValue, newValue) {
3182
+ if (!this.isConnected) return;
3183
+ if (name === "open") {
3184
+ const shouldBeOpen = newValue !== null && newValue !== "false";
3185
+ if (shouldBeOpen && !this._isOpen) this._doOpen();
3186
+ else if (!shouldBeOpen && this._isOpen) this._doClose();
3187
+ } else if (name === "placement" && this._isOpen) this._position();
3188
+ }
3189
+ get open() {
3190
+ const attr = this.getAttribute("open");
3191
+ return attr !== null && attr !== "false";
3192
+ }
3193
+ set open(value) {
3194
+ this.toggleAttribute("open", value);
3195
+ }
3196
+ get _isOpen() {
3197
+ return this.hasAttribute("data-open");
3198
+ }
3199
+ get _placement() {
3200
+ return this.getAttribute("placement") ?? "top";
3201
+ }
3202
+ _wire() {
3203
+ this._content = this.querySelector(":scope > .ix-popover__content");
3204
+ this._trigger = Array.from(this.children).find((child) => child instanceof HTMLElement && !child.classList.contains("ix-popover__content")) ?? null;
3205
+ if (!this._trigger || !this._content) return;
3206
+ if (!this._content.id) this._content.id = `ix-popover-content-${this._instanceId}`;
3207
+ this._trigger.setAttribute("aria-haspopup", "dialog");
3208
+ this._trigger.setAttribute("aria-expanded", "false");
3209
+ this._trigger.setAttribute("aria-controls", this._content.id);
3210
+ this._content.hidden = true;
3211
+ this._content.setAttribute("role", "dialog");
3212
+ this._onTriggerClick = (e) => {
3213
+ e.preventDefault();
3214
+ e.stopPropagation();
3215
+ this._toggleOpen();
3216
+ };
3217
+ this._trigger.addEventListener("click", this._onTriggerClick);
3218
+ this._onTriggerKeydown = (e) => this._handleTriggerKeydown(e);
3219
+ this._trigger.addEventListener("keydown", this._onTriggerKeydown);
3220
+ this._onContentKeydown = (e) => this._handleContentKeydown(e);
3221
+ this._content.addEventListener("keydown", this._onContentKeydown);
3222
+ this._onDocPointer = (e) => {
3223
+ if (!this._isOpen) return;
3224
+ const target = e.target;
3225
+ if (!this.contains(target)) this._close();
3226
+ };
3227
+ document.addEventListener("pointerdown", this._onDocPointer, true);
3228
+ this._onFocusout = (e) => {
3229
+ if (!this._isOpen) return;
3230
+ const relatedTarget = e.relatedTarget;
3231
+ if (relatedTarget && this.contains(relatedTarget)) return;
3232
+ this._close();
3233
+ };
3234
+ this.addEventListener("focusout", this._onFocusout);
3235
+ if (this.open) this._doOpen();
3236
+ }
3237
+ _handleTriggerKeydown(e) {
3238
+ switch (e.key) {
3239
+ case "Enter":
3240
+ case " ":
3241
+ e.preventDefault();
3242
+ this._toggleOpen();
3243
+ break;
3244
+ case "ArrowDown": if (!this._isOpen) {
3245
+ e.preventDefault();
3246
+ this._open();
3247
+ }
3248
+ }
3249
+ }
3250
+ _handleContentKeydown(e) {
3251
+ if (e.key === "Escape") {
3252
+ e.preventDefault();
3253
+ e.stopPropagation();
3254
+ this._close();
3255
+ this._trigger?.focus();
3256
+ }
3257
+ }
3258
+ _open() {
3259
+ if (this._isOpen || !this._content || !this._trigger) return;
3260
+ if (this._isControlled) {
3261
+ this.dispatchEvent(new CustomEvent("open-request", { bubbles: true }));
3262
+ return;
3263
+ }
3264
+ this._doOpen();
3265
+ }
3266
+ _doOpen() {
3267
+ if (this._isOpen || !this._content || !this._trigger) return;
3268
+ this.setAttribute("data-open", "");
3269
+ this.setAttribute("open", "");
3270
+ this._content.hidden = false;
3271
+ this._trigger.setAttribute("aria-expanded", "true");
3272
+ this._position();
3273
+ this._addRepositionListeners();
3274
+ const firstFocusable = this._content.querySelector("button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])");
3275
+ if (firstFocusable) firstFocusable.focus();
3276
+ this.dispatchEvent(new CustomEvent("open", { bubbles: true }));
3277
+ }
3278
+ _close() {
3279
+ if (!this._isOpen || !this._content || !this._trigger) return;
3280
+ if (this._isControlled) {
3281
+ this.dispatchEvent(new CustomEvent("close-request", { bubbles: true }));
3282
+ return;
3283
+ }
3284
+ this._doClose();
3285
+ }
3286
+ _doClose() {
3287
+ if (!this._isOpen || !this._content || !this._trigger) return;
3288
+ this.removeAttribute("data-open");
3289
+ this.removeAttribute("open");
3290
+ this._content.hidden = true;
3291
+ this._trigger.setAttribute("aria-expanded", "false");
3292
+ this._removeRepositionListeners();
3293
+ this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
3294
+ }
3295
+ _toggleOpen() {
3296
+ if (this._isOpen) this._close();
3297
+ else this._open();
3298
+ }
3299
+ _position() {
3300
+ if (!this._content || !this._trigger) return;
3301
+ const triggerRect = this._trigger.getBoundingClientRect();
3302
+ const contentRect = this._content.getBoundingClientRect();
3303
+ const offsetRect = this._getFixedOffsetParent()?.getBoundingClientRect() ?? {
3304
+ top: 0,
3305
+ left: 0
3306
+ };
3307
+ const vw = window.innerWidth;
3308
+ const vh = window.innerHeight;
3309
+ const gap = 8;
3310
+ const arrowSize = 8;
3311
+ let top;
3312
+ let left;
3313
+ let actualPlacement = this._placement;
3314
+ const spaceAbove = triggerRect.top;
3315
+ const spaceBelow = vh - triggerRect.bottom;
3316
+ const spaceLeft = triggerRect.left;
3317
+ const spaceRight = vw - triggerRect.right;
3318
+ if (actualPlacement === "top" && spaceAbove < contentRect.height + gap + arrowSize) {
3319
+ if (spaceBelow >= contentRect.height + gap + arrowSize) actualPlacement = "bottom";
3320
+ } else if (actualPlacement === "bottom" && spaceBelow < contentRect.height + gap + arrowSize) {
3321
+ if (spaceAbove >= contentRect.height + gap + arrowSize) actualPlacement = "top";
3322
+ } else if (actualPlacement === "left" && spaceLeft < contentRect.width + gap + arrowSize) {
3323
+ if (spaceRight >= contentRect.width + gap + arrowSize) actualPlacement = "right";
3324
+ } else if (actualPlacement === "right" && spaceRight < contentRect.width + gap + arrowSize) {
3325
+ if (spaceLeft >= contentRect.width + gap + arrowSize) actualPlacement = "left";
3326
+ }
3327
+ const triggerCenterX = triggerRect.left + triggerRect.width / 2;
3328
+ const triggerCenterY = triggerRect.top + triggerRect.height / 2;
3329
+ switch (actualPlacement) {
3330
+ case "top":
3331
+ top = triggerRect.top - contentRect.height - gap - arrowSize;
3332
+ left = triggerCenterX - contentRect.width / 2;
3333
+ break;
3334
+ case "bottom":
3335
+ top = triggerRect.bottom + gap + arrowSize;
3336
+ left = triggerCenterX - contentRect.width / 2;
3337
+ break;
3338
+ case "left":
3339
+ top = triggerCenterY - contentRect.height / 2;
3340
+ left = triggerRect.left - contentRect.width - gap - arrowSize;
3341
+ break;
3342
+ case "right":
3343
+ top = triggerCenterY - contentRect.height / 2;
3344
+ left = triggerRect.right + gap + arrowSize;
3345
+ break;
3346
+ default:
3347
+ top = triggerRect.top - contentRect.height - gap - arrowSize;
3348
+ left = triggerCenterX - contentRect.width / 2;
3349
+ }
3350
+ if (left + contentRect.width > vw - gap) left = vw - contentRect.width - gap;
3351
+ if (left < gap) left = gap;
3352
+ if (top + contentRect.height > vh - gap) top = Math.max(gap, vh - contentRect.height - gap);
3353
+ if (top < gap) top = gap;
3354
+ top -= offsetRect.top;
3355
+ left -= offsetRect.left;
3356
+ this._content.style.top = `${top}px`;
3357
+ this._content.style.left = `${left}px`;
3358
+ this._content.setAttribute("data-placement", actualPlacement);
3359
+ const adjustedTriggerCenterX = triggerCenterX - offsetRect.left;
3360
+ const adjustedTriggerCenterY = triggerCenterY - offsetRect.top;
3361
+ let arrowX;
3362
+ let arrowY;
3363
+ if (actualPlacement === "top" || actualPlacement === "bottom") {
3364
+ arrowX = adjustedTriggerCenterX - left;
3365
+ arrowX = Math.max(16, Math.min(arrowX, contentRect.width - arrowSize - gap));
3366
+ this._content.style.setProperty("--_arrow-x", `${arrowX}px`);
3367
+ this._content.style.removeProperty("--_arrow-y");
3368
+ } else {
3369
+ arrowY = adjustedTriggerCenterY - top;
3370
+ arrowY = Math.max(16, Math.min(arrowY, contentRect.height - arrowSize - gap));
3371
+ this._content.style.setProperty("--_arrow-y", `${arrowY}px`);
3372
+ this._content.style.removeProperty("--_arrow-x");
3373
+ }
3374
+ }
3375
+ /**
3376
+ * Finner nærmeste ancestor med transform, filter, eller will-change som
3377
+ * skaper en ny containing block for position: fixed.
3378
+ */
3379
+ _getFixedOffsetParent() {
3380
+ let el = this._content?.parentElement ?? null;
3381
+ while (el && el !== document.documentElement) {
3382
+ const style = getComputedStyle(el);
3383
+ const transform = style.transform;
3384
+ const willChange = style.willChange;
3385
+ const filter = style.filter;
3386
+ if (transform && transform !== "none" || willChange && (willChange.includes("transform") || willChange.includes("filter")) || filter && filter !== "none") return el;
3387
+ el = el.parentElement;
3388
+ }
3389
+ return null;
3390
+ }
3391
+ _addRepositionListeners() {
3392
+ if (this._onReposition) return;
3393
+ this._onReposition = () => {
3394
+ if (this._isOpen) this._position();
3395
+ };
3396
+ window.addEventListener("scroll", this._onReposition, true);
3397
+ window.addEventListener("resize", this._onReposition);
3398
+ }
3399
+ _removeRepositionListeners() {
3400
+ if (this._onReposition) {
3401
+ window.removeEventListener("scroll", this._onReposition, true);
3402
+ window.removeEventListener("resize", this._onReposition);
3403
+ this._onReposition = null;
3404
+ }
3405
+ }
3406
+ };
3407
+ //#endregion
3087
3408
  //#region lib/utils/platform.ts
3088
3409
  /**
3089
3410
  * Plattform-deteksjon for web components.
@@ -3992,6 +4313,7 @@ customElements.define("ix-combobox", IxCombobox);
3992
4313
  customElements.define("ix-date-field", IxDateField);
3993
4314
  customElements.define("ix-dropdown", IxDropdown);
3994
4315
  customElements.define("ix-phone-number-field", IxPhoneNumberField);
4316
+ customElements.define("ix-popover", IxPopover);
3995
4317
  customElements.define("ix-progress-bar", IxProgressBar);
3996
4318
  customElements.define("ix-tabs", IxTabs);
3997
4319
  customElements.define("ix-tab-list", IxTabList);
@@ -3999,4 +4321,4 @@ customElements.define("ix-tab", IxTab);
3999
4321
  customElements.define("ix-tab-panel", IxTabPanel);
4000
4322
  globalThis.cn = cn;
4001
4323
  //#endregion
4002
- export { BUILTIN_FORMAT_NAMES, COMMON_ICON_NAMES, IxCheckboxGroup, IxCombobox, IxDateField, IxDropdown, IxField, IxIcon, IxPhoneNumberField, IxProgressBar, IxRadioGroup, IxTab, IxTabList, IxTabPanel, IxTabs, amountFormatterForLocale, cn, createAmountFormatter, createPatternFormatter, registerFormat, resolveFormat };
4324
+ 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 };
@@ -74,6 +74,7 @@ export declare class IxDropdown extends HTMLElement {
74
74
  private _handleTriggerKeydown;
75
75
  private _handleMenuKeydown;
76
76
  private _handleItemPointerEnter;
77
+ private _closeOtherSubmenus;
77
78
  private _handleItemClick;
78
79
  private _handleTypeahead;
79
80
  private _focusItem;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * IxPopover — ARIA-lim for en popover med kontekstuell informasjon eller handlinger.
3
+ *
4
+ * ## Designfilosofi
5
+ *
6
+ * Komponenten kobler en trigger-knapp med et innholdspanel. Forfatteren skriver
7
+ * en <button> (trigger) og en .ix-popover__content i light DOM. Komponentens jobb
8
+ * er å sette opp ARIA-attributter, håndtere åpne/lukke-logikk, posisjonere panelet,
9
+ * og styre fokus.
10
+ *
11
+ * ## Fokushåndtering
12
+ *
13
+ * Når popoveren åpnes, flyttes fokus til første focusable element i content (hvis det
14
+ * finnes). Når popoveren lukkes med Escape, returneres fokus til trigger. Fokus trappes IKKE —
15
+ * brukeren kan tabbe ut av popoveren naturlig.
16
+ *
17
+ * ## Posisjonering
18
+ *
19
+ * Panelet posisjoneres manuelt med getBoundingClientRect + viewport-clamp
20
+ * (samme grep som dropdown og tooltip) — position: fixed, JS setter top/left.
21
+ * Auto-flip brukes hvis det ikke er nok plass i ønsket retning.
22
+ *
23
+ * @example
24
+ * <ix-popover placement="top">
25
+ * <button class="ix-button">Åpne popover</button>
26
+ * <div class="ix-popover__content">
27
+ * <div class="ix-popover__heading">Overskrift</div>
28
+ * <div class="ix-popover__body">Forklarende tekst her.</div>
29
+ * <div class="ix-popover__actions">
30
+ * <button class="ix-button" data-size="sm">OK</button>
31
+ * </div>
32
+ * </div>
33
+ * </ix-popover>
34
+ */
35
+ export declare class IxPopover extends HTMLElement {
36
+ private _instanceId;
37
+ private _trigger;
38
+ private _content;
39
+ private _onTriggerClick;
40
+ private _onTriggerKeydown;
41
+ private _onContentKeydown;
42
+ private _onDocPointer;
43
+ private _onFocusout;
44
+ private _onReposition;
45
+ static get observedAttributes(): string[];
46
+ /**
47
+ * Kontrollert modus: når `data-controlled` er satt, endrer ikke komponenten
48
+ * egen åpen-tilstand. Den dispatcher kun `open-request` / `close-request`,
49
+ * og foreldren må sette `open`-attributtet for å faktisk åpne/lukke.
50
+ */
51
+ private get _isControlled();
52
+ connectedCallback(): void;
53
+ disconnectedCallback(): void;
54
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
55
+ get open(): boolean;
56
+ set open(value: boolean);
57
+ private get _isOpen();
58
+ private get _placement();
59
+ private _wire;
60
+ private _handleTriggerKeydown;
61
+ private _handleContentKeydown;
62
+ private _open;
63
+ private _doOpen;
64
+ private _close;
65
+ private _doClose;
66
+ private _toggleOpen;
67
+ private _position;
68
+ /**
69
+ * Finner nærmeste ancestor med transform, filter, eller will-change som
70
+ * skaper en ny containing block for position: fixed.
71
+ */
72
+ private _getFixedOffsetParent;
73
+ private _addRepositionListeners;
74
+ private _removeRepositionListeners;
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sb1/indeks-web",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "Web components for Indeks design system",
6
6
  "homepage": "https://design.sparebank1.no",
@@ -40,10 +40,10 @@
40
40
  "author": "",
41
41
  "license": "MIT",
42
42
  "devDependencies": {
43
- "@types/node": "^26.0.1",
43
+ "@types/node": "^26.2.0",
44
44
  "jsdom": "^29.1.1",
45
45
  "typescript": "~6.0.3",
46
- "vite": "^8.1.5",
46
+ "vite": "^8.2.1",
47
47
  "vite-plugin-dts": "^5.0.3",
48
48
  "vitest": "^4.1.10"
49
49
  },