@sb1/indeks-web 0.18.1 → 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,28 @@
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
+
20
+ ## 0.19.0
21
+
22
+ ### Minor Changes
23
+
24
+ - ef4f6e2: Ny DropdownMenu-komponent for handlingsmenyer og navigasjon. Komponenten følger APG Menu Button-mønsteret med full tastaturstøtte (piltaster, Home/End, Escape, typeahead), submenyer, og automatisk ARIA-oppsett. CSS-laget definerer `.ix-dropdown__menu`, `.ix-dropdown__item`, `.ix-dropdown__divider` og `.ix-dropdown__group`. Web component `<ix-dropdown>` håndterer åpne/lukke, posisjonering og fokusstyring. React-wrapperen `<DropdownMenu>` eksponerer `<DropdownMenu.Trigger>`, `<DropdownMenu.Menu>`, `<DropdownMenu.Item>`, `<DropdownMenu.Divider>` og `<DropdownMenu.Group>`.
25
+
3
26
  ## 0.18.1
4
27
 
5
28
  ## 0.18.0
@@ -1,9 +1,11 @@
1
1
  import { IxCheckboxGroup } from './lib/components/checkbox-group/IxCheckboxGroup.js';
2
2
  import { IxCombobox } from './lib/components/combobox/IxCombobox.js';
3
3
  import { IxDateField } from './lib/components/date-field/IxDateField.js';
4
+ import { IxDropdown } from './lib/components/dropdown/IxDropdown.js';
4
5
  import { IxField } from './lib/components/field/IxField.js';
5
6
  import { IxIcon, IconName } from './lib/components/icon/IxIcon.js';
6
7
  import { IxPhoneNumberField } from './lib/components/phone-number-field/IxPhoneNumberField.js';
8
+ import { IxPopover } from './lib/components/popover/IxPopover.js';
7
9
  import { IxProgressBar } from './lib/components/progress-bar/IxProgressBar.js';
8
10
  import { IxRadioGroup } from './lib/components/radio-group/IxRadioGroup.js';
9
11
  import { IxTabs, IxTabList, IxTab, IxTabPanel } from './lib/components/tabs/IxTabs.js';
@@ -13,7 +15,9 @@ export { IxRadioGroup };
13
15
  export { IxCheckboxGroup };
14
16
  export { IxCombobox };
15
17
  export { IxDateField };
18
+ export { IxDropdown };
16
19
  export { IxPhoneNumberField };
20
+ export { IxPopover };
17
21
  export { IxProgressBar };
18
22
  export type { ProgressBarState } from './lib/components/progress-bar/IxProgressBar.js';
19
23
  export { IxTabs, IxTabList, IxTab, IxTabPanel };
@@ -32,7 +36,9 @@ declare global {
32
36
  'ix-checkbox-group': IxCheckboxGroup;
33
37
  'ix-combobox': IxCombobox;
34
38
  'ix-date-field': IxDateField;
39
+ 'ix-dropdown': IxDropdown;
35
40
  'ix-phone-number-field': IxPhoneNumberField;
41
+ 'ix-popover': IxPopover;
36
42
  'ix-progress-bar': IxProgressBar;
37
43
  'ix-tabs': IxTabs;
38
44
  'ix-tab-list': IxTabList;
@@ -101,6 +107,19 @@ declare global {
101
107
  class?: string;
102
108
  children?: unknown;
103
109
  };
110
+ 'ix-dropdown': {
111
+ open?: boolean | '';
112
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
113
+ 'data-submenu'?: boolean | '';
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
+ };
104
123
  'ix-icon': {
105
124
  name?: IconName;
106
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,
@@ -506,7 +506,8 @@ var IxCombobox = class extends HTMLElement {
506
506
  listbox.style.left = `${Math.max(8, r.left)}px`;
507
507
  const ph = listbox.offsetHeight;
508
508
  const spaceBelow = vh - r.bottom;
509
- if (ph > spaceBelow && r.top > spaceBelow) listbox.style.top = `${Math.max(8, r.top - ph - gap)}px`;
509
+ const spaceAbove = r.top;
510
+ if (ph > spaceBelow && spaceAbove > spaceBelow) listbox.style.top = `${Math.max(8, r.top - ph - gap)}px`;
510
511
  else listbox.style.top = `${r.bottom + gap}px`;
511
512
  }
512
513
  _addRepositionListeners() {
@@ -619,16 +620,14 @@ var IxCombobox = class extends HTMLElement {
619
620
  }
620
621
  break;
621
622
  case "ArrowLeft":
622
- case "Backspace":
623
- 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();
629
- }
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();
630
629
  }
631
- break;
630
+ }
632
631
  }
633
632
  }
634
633
  _handleChipKeydown(e, chip) {
@@ -1321,6 +1320,503 @@ var IxDateField = class extends HTMLElement {
1321
1320
  }
1322
1321
  };
1323
1322
  //#endregion
1323
+ //#region lib/components/dropdown/IxDropdown.ts
1324
+ /**
1325
+ * IxDropdown — ARIA-lim for en nedtrekksmeny med handlinger eller navigasjon.
1326
+ *
1327
+ * ## Designfilosofi
1328
+ *
1329
+ * Komponenten kobler en trigger-knapp med en meny. Forfatteren skriver en
1330
+ * <button> (trigger) og en .ix-dropdown__menu med .ix-dropdown__item-elementer
1331
+ * i light DOM. Komponentens jobb er å sette opp ARIA-attributter, håndtere
1332
+ * tastaturnavigasjon, posisjonere menyen, og styre åpen/lukket-tilstand.
1333
+ *
1334
+ * ## Roving tabindex
1335
+ *
1336
+ * Alle items har tabindex="-1" unntatt det aktive (fokuserte). Piltaster
1337
+ * flytter fokus mellom items. Dette følger APG Menu Button-mønsteret.
1338
+ *
1339
+ * ## Submenyer
1340
+ *
1341
+ * En submenu er en nøstet <ix-dropdown data-submenu> inne i parent-menyen.
1342
+ * ArrowRight på en submenu-trigger åpner submenyen, ArrowLeft lukker den.
1343
+ * Submenyen posisjoneres til høyre for parent-item (flipper ved plassmangel).
1344
+ *
1345
+ * ## Posisjonering
1346
+ *
1347
+ * Menyen posisjoneres manuelt med getBoundingClientRect + viewport-clamp
1348
+ * (samme grep som tooltip og combobox) — position: fixed, JS setter top/left.
1349
+ *
1350
+ * @example
1351
+ * <ix-dropdown>
1352
+ * <button class="ix-button" data-variant="secondary">Handlinger</button>
1353
+ * <div class="ix-dropdown__menu">
1354
+ * <button class="ix-dropdown__item">Rediger</button>
1355
+ * <button class="ix-dropdown__item">Dupliser</button>
1356
+ * <hr class="ix-dropdown__divider" />
1357
+ * <button class="ix-dropdown__item" data-danger>Slett</button>
1358
+ * </div>
1359
+ * </ix-dropdown>
1360
+ */
1361
+ var dropdownCounter = 0;
1362
+ var IxDropdown = class extends HTMLElement {
1363
+ constructor(..._args) {
1364
+ super(..._args);
1365
+ _defineProperty(this, "_instanceId", 0);
1366
+ _defineProperty(this, "_trigger", null);
1367
+ _defineProperty(this, "_menu", null);
1368
+ _defineProperty(this, "_items", []);
1369
+ _defineProperty(this, "_activeIndex", -1);
1370
+ _defineProperty(this, "_onTriggerClick", null);
1371
+ _defineProperty(this, "_onTriggerKeydown", null);
1372
+ _defineProperty(this, "_onMenuKeydown", null);
1373
+ _defineProperty(this, "_onMenuClick", null);
1374
+ _defineProperty(this, "_onMenuPointerenter", null);
1375
+ _defineProperty(this, "_onDocPointer", null);
1376
+ _defineProperty(this, "_onFocusout", null);
1377
+ _defineProperty(this, "_onReposition", null);
1378
+ _defineProperty(this, "_itemObserver", null);
1379
+ _defineProperty(this, "_typeaheadBuffer", "");
1380
+ _defineProperty(this, "_typeaheadTimer", null);
1381
+ _defineProperty(this, "_openedWithKeyboard", false);
1382
+ }
1383
+ static get observedAttributes() {
1384
+ return [
1385
+ "open",
1386
+ "placement",
1387
+ "data-controlled"
1388
+ ];
1389
+ }
1390
+ /**
1391
+ * Kontrollert modus: når `data-controlled` er satt, endrer ikke komponenten
1392
+ * egen åpen-tilstand. Den dispatcher kun `open-request` / `close-request`,
1393
+ * og foreldren må sette `open`-attributtet for å faktisk åpne/lukke.
1394
+ */
1395
+ get _isControlled() {
1396
+ return this.hasAttribute("data-controlled");
1397
+ }
1398
+ connectedCallback() {
1399
+ this._instanceId = ++dropdownCounter;
1400
+ this._wire();
1401
+ }
1402
+ disconnectedCallback() {
1403
+ if (this._trigger) {
1404
+ if (this._onTriggerClick) this._trigger.removeEventListener("click", this._onTriggerClick);
1405
+ if (this._onTriggerKeydown) this._trigger.removeEventListener("keydown", this._onTriggerKeydown);
1406
+ }
1407
+ if (this._menu) {
1408
+ if (this._onMenuKeydown) this._menu.removeEventListener("keydown", this._onMenuKeydown);
1409
+ if (this._onMenuClick) this._menu.removeEventListener("click", this._onMenuClick);
1410
+ if (this._onMenuPointerenter) this._menu.removeEventListener("pointerenter", this._onMenuPointerenter, true);
1411
+ }
1412
+ if (this._onDocPointer) document.removeEventListener("pointerdown", this._onDocPointer, true);
1413
+ if (this._onFocusout) this.removeEventListener("focusout", this._onFocusout);
1414
+ this._removeRepositionListeners();
1415
+ this._itemObserver?.disconnect();
1416
+ if (this._typeaheadTimer) {
1417
+ clearTimeout(this._typeaheadTimer);
1418
+ this._typeaheadTimer = null;
1419
+ }
1420
+ this._onTriggerClick = null;
1421
+ this._onTriggerKeydown = null;
1422
+ this._onMenuKeydown = null;
1423
+ this._onMenuClick = null;
1424
+ this._onMenuPointerenter = null;
1425
+ this._onDocPointer = null;
1426
+ this._onFocusout = null;
1427
+ this._onReposition = null;
1428
+ this._itemObserver = null;
1429
+ this._trigger = null;
1430
+ this._menu = null;
1431
+ this._items = [];
1432
+ }
1433
+ attributeChangedCallback(name, _oldValue, newValue) {
1434
+ if (!this.isConnected) return;
1435
+ if (name === "open") {
1436
+ const shouldBeOpen = newValue !== null && newValue !== "false";
1437
+ if (shouldBeOpen && !this._isOpen) this._doOpen();
1438
+ else if (!shouldBeOpen && this._isOpen) this._doClose();
1439
+ } else if (name === "placement" && this._isOpen) this._position();
1440
+ }
1441
+ get open() {
1442
+ const attr = this.getAttribute("open");
1443
+ return attr !== null && attr !== "false";
1444
+ }
1445
+ set open(value) {
1446
+ this.toggleAttribute("open", value);
1447
+ }
1448
+ get _isOpen() {
1449
+ return this.hasAttribute("data-open");
1450
+ }
1451
+ get _isSubmenu() {
1452
+ return this.hasAttribute("data-submenu");
1453
+ }
1454
+ get _placement() {
1455
+ return this.getAttribute("placement") ?? "bottom-start";
1456
+ }
1457
+ _wire() {
1458
+ this._menu = this.querySelector(":scope > .ix-dropdown__menu");
1459
+ if (this._isSubmenu) this._trigger = this.querySelector(":scope > .ix-dropdown__item");
1460
+ else this._trigger = Array.from(this.children).find((child) => child instanceof HTMLElement && !child.classList.contains("ix-dropdown__menu")) ?? null;
1461
+ if (!this._trigger || !this._menu) return;
1462
+ if (!this._menu.id) this._menu.id = `ix-dropdown-menu-${this._instanceId}`;
1463
+ if (!this._menu.getAttribute("role")) this._menu.setAttribute("role", "menu");
1464
+ this._menu.hidden = true;
1465
+ this._trigger.setAttribute("aria-haspopup", "menu");
1466
+ this._trigger.setAttribute("aria-expanded", "false");
1467
+ this._trigger.setAttribute("aria-controls", this._menu.id);
1468
+ this._wireItems();
1469
+ this._onTriggerClick = (e) => {
1470
+ e.preventDefault();
1471
+ e.stopPropagation();
1472
+ this._openedWithKeyboard = false;
1473
+ this._toggleOpen();
1474
+ };
1475
+ this._trigger.addEventListener("click", this._onTriggerClick);
1476
+ this._onTriggerKeydown = (e) => this._handleTriggerKeydown(e);
1477
+ this._trigger.addEventListener("keydown", this._onTriggerKeydown);
1478
+ this._onMenuKeydown = (e) => this._handleMenuKeydown(e);
1479
+ this._menu.addEventListener("keydown", this._onMenuKeydown);
1480
+ this._onMenuClick = (e) => {
1481
+ const item = e.target.closest(".ix-dropdown__item");
1482
+ if (item && this._menu?.contains(item)) this._handleItemClick(e, item);
1483
+ };
1484
+ this._menu.addEventListener("click", this._onMenuClick);
1485
+ this._onMenuPointerenter = (e) => {
1486
+ const item = e.target.closest(".ix-dropdown__item");
1487
+ if (item && this._menu?.contains(item)) {
1488
+ const idx = this._items.indexOf(item);
1489
+ if (idx !== -1) this._handleItemPointerEnter(idx);
1490
+ }
1491
+ };
1492
+ this._menu.addEventListener("pointerenter", this._onMenuPointerenter, true);
1493
+ this._onDocPointer = (e) => {
1494
+ if (!this._isOpen) return;
1495
+ const target = e.target;
1496
+ if (!this.contains(target)) this._close();
1497
+ };
1498
+ document.addEventListener("pointerdown", this._onDocPointer, true);
1499
+ this._onFocusout = (e) => {
1500
+ if (!this._isOpen) return;
1501
+ const relatedTarget = e.relatedTarget;
1502
+ if (relatedTarget && this.contains(relatedTarget)) return;
1503
+ const rootDropdown = this.closest("ix-dropdown:not([data-submenu])") ?? this;
1504
+ if (relatedTarget && rootDropdown.contains(relatedTarget)) return;
1505
+ this._close();
1506
+ };
1507
+ this.addEventListener("focusout", this._onFocusout);
1508
+ this._itemObserver = new MutationObserver(() => this._rewireItems());
1509
+ this._itemObserver.observe(this._menu, {
1510
+ childList: true,
1511
+ subtree: true
1512
+ });
1513
+ if (this.open) this._doOpen();
1514
+ }
1515
+ _wireItems() {
1516
+ this._rewireItems();
1517
+ }
1518
+ _rewireItems() {
1519
+ if (!this._menu) return;
1520
+ const items = Array.from(this._menu.children).filter((child) => child instanceof HTMLElement && child.classList.contains("ix-dropdown__item"));
1521
+ Array.from(this._menu.querySelectorAll(":scope > ix-dropdown[data-submenu]")).forEach((submenu) => {
1522
+ const submenuTrigger = submenu.querySelector(":scope > .ix-dropdown__item");
1523
+ if (submenuTrigger) {
1524
+ items.push(submenuTrigger);
1525
+ submenuTrigger.setAttribute("aria-haspopup", "menu");
1526
+ submenuTrigger.setAttribute("aria-expanded", "false");
1527
+ }
1528
+ });
1529
+ items.sort((a, b) => {
1530
+ const position = a.compareDocumentPosition(b);
1531
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
1532
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
1533
+ return 0;
1534
+ });
1535
+ items.forEach((item) => {
1536
+ if (!item.getAttribute("role")) item.setAttribute("role", "menuitem");
1537
+ item.setAttribute("tabindex", "-1");
1538
+ });
1539
+ this._menu.querySelectorAll(":scope > .ix-dropdown__divider").forEach((divider) => {
1540
+ if (!divider.getAttribute("role")) divider.setAttribute("role", "separator");
1541
+ });
1542
+ this._items = items;
1543
+ }
1544
+ _handleTriggerKeydown(e) {
1545
+ if (this._isSubmenu) return;
1546
+ switch (e.key) {
1547
+ case "Enter":
1548
+ case " ":
1549
+ case "ArrowDown":
1550
+ e.preventDefault();
1551
+ this._openedWithKeyboard = true;
1552
+ this._open();
1553
+ break;
1554
+ case "ArrowUp":
1555
+ e.preventDefault();
1556
+ this._openedWithKeyboard = true;
1557
+ this._open();
1558
+ this._focusItem(this._items.length - 1);
1559
+ }
1560
+ }
1561
+ _handleMenuKeydown(e) {
1562
+ const target = e.target;
1563
+ if (!this._menu?.contains(target)) return;
1564
+ const closestSubmenuMenu = target.closest("ix-dropdown[data-submenu] > .ix-dropdown__menu");
1565
+ if (closestSubmenuMenu && closestSubmenuMenu !== this._menu && this._menu.contains(closestSubmenuMenu)) return;
1566
+ switch (e.key) {
1567
+ case "Escape":
1568
+ e.preventDefault();
1569
+ e.stopPropagation();
1570
+ this._close();
1571
+ this._trigger?.focus();
1572
+ break;
1573
+ case "ArrowDown":
1574
+ e.preventDefault();
1575
+ this._focusNextItem(1);
1576
+ break;
1577
+ case "ArrowUp":
1578
+ e.preventDefault();
1579
+ this._focusNextItem(-1);
1580
+ break;
1581
+ case "Home":
1582
+ e.preventDefault();
1583
+ this._focusItem(0);
1584
+ break;
1585
+ case "End":
1586
+ e.preventDefault();
1587
+ this._focusItem(this._items.length - 1);
1588
+ break;
1589
+ case "ArrowRight": {
1590
+ const submenu = target.closest("ix-dropdown[data-submenu]");
1591
+ if (submenu && submenu !== this && target === submenu._trigger) {
1592
+ e.preventDefault();
1593
+ submenu._openedWithKeyboard = true;
1594
+ submenu._open();
1595
+ }
1596
+ break;
1597
+ }
1598
+ case "ArrowLeft":
1599
+ if (this._isSubmenu) {
1600
+ e.preventDefault();
1601
+ e.stopPropagation();
1602
+ const parentDropdown = this.parentElement?.closest("ix-dropdown");
1603
+ if (this._trigger) {
1604
+ this._trigger.focus();
1605
+ if (parentDropdown) {
1606
+ const triggerIndex = parentDropdown._items.indexOf(this._trigger);
1607
+ if (triggerIndex !== -1) parentDropdown._focusItem(triggerIndex);
1608
+ }
1609
+ }
1610
+ this._close();
1611
+ }
1612
+ break;
1613
+ case "Enter":
1614
+ case " ":
1615
+ if (target.classList.contains("ix-dropdown__item")) {
1616
+ e.preventDefault();
1617
+ const submenu = target.parentElement;
1618
+ if (submenu?.hasAttribute("data-submenu") && target === submenu._trigger) {
1619
+ submenu._openedWithKeyboard = true;
1620
+ submenu._open();
1621
+ } else target.click();
1622
+ }
1623
+ break;
1624
+ case "Tab":
1625
+ e.preventDefault();
1626
+ this._closeAll();
1627
+ break;
1628
+ default: if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) this._handleTypeahead(e.key);
1629
+ }
1630
+ }
1631
+ _handleItemPointerEnter(index) {
1632
+ this._activeIndex = index;
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
+ });
1646
+ }
1647
+ _handleItemClick(e, item) {
1648
+ if (item.hasAttribute("disabled") || item.getAttribute("aria-disabled") === "true") {
1649
+ e.preventDefault();
1650
+ return;
1651
+ }
1652
+ const submenu = item.parentElement;
1653
+ if (submenu?.hasAttribute("data-submenu") && item === submenu._trigger) {
1654
+ e.stopPropagation();
1655
+ return;
1656
+ }
1657
+ this._activeIndex = this._items.indexOf(item);
1658
+ this._closeAll();
1659
+ }
1660
+ _handleTypeahead(char) {
1661
+ this._typeaheadBuffer += char.toLowerCase();
1662
+ if (this._typeaheadTimer) clearTimeout(this._typeaheadTimer);
1663
+ this._typeaheadTimer = setTimeout(() => {
1664
+ this._typeaheadBuffer = "";
1665
+ }, 500);
1666
+ const match = this._items.findIndex((item) => {
1667
+ if (item.hasAttribute("disabled") || item.getAttribute("aria-disabled") === "true") return false;
1668
+ return (item.textContent ?? "").trim().toLowerCase().startsWith(this._typeaheadBuffer);
1669
+ });
1670
+ if (match !== -1) this._focusItem(match);
1671
+ }
1672
+ _focusItem(index) {
1673
+ if (this._items.filter((item) => !item.hasAttribute("disabled") && item.getAttribute("aria-disabled") !== "true").length === 0) return;
1674
+ const clampedIndex = Math.max(0, Math.min(index, this._items.length - 1));
1675
+ const item = this._items[clampedIndex];
1676
+ if (item?.hasAttribute("disabled") || item?.getAttribute("aria-disabled") === "true") {
1677
+ this._focusNextItem(1);
1678
+ return;
1679
+ }
1680
+ this._items.forEach((i, idx) => {
1681
+ i.setAttribute("tabindex", idx === clampedIndex ? "0" : "-1");
1682
+ i.removeAttribute("data-active");
1683
+ });
1684
+ if (item) {
1685
+ item.setAttribute("data-active", "");
1686
+ item.focus();
1687
+ this._activeIndex = clampedIndex;
1688
+ }
1689
+ }
1690
+ _focusNextItem(delta) {
1691
+ if (this._items.filter((item) => !item.hasAttribute("disabled") && item.getAttribute("aria-disabled") !== "true").length === 0) return;
1692
+ let currentIdx = this._activeIndex;
1693
+ if (currentIdx === -1) currentIdx = delta > 0 ? -1 : this._items.length;
1694
+ let nextIdx = currentIdx;
1695
+ let iterations = 0;
1696
+ do {
1697
+ nextIdx = nextIdx + delta;
1698
+ if (nextIdx < 0) nextIdx = this._items.length - 1;
1699
+ if (nextIdx >= this._items.length) nextIdx = 0;
1700
+ iterations++;
1701
+ } while (iterations < this._items.length && (this._items[nextIdx]?.hasAttribute("disabled") || this._items[nextIdx]?.getAttribute("aria-disabled") === "true"));
1702
+ this._focusItem(nextIdx);
1703
+ }
1704
+ _open() {
1705
+ if (this._isOpen || !this._menu || !this._trigger) return;
1706
+ if (this._isControlled) {
1707
+ this.dispatchEvent(new CustomEvent("open-request", { bubbles: true }));
1708
+ return;
1709
+ }
1710
+ this._doOpen();
1711
+ }
1712
+ _doOpen() {
1713
+ if (this._isOpen || !this._menu || !this._trigger) return;
1714
+ this.setAttribute("data-open", "");
1715
+ this.setAttribute("open", "");
1716
+ this._menu.hidden = false;
1717
+ this._trigger.setAttribute("aria-expanded", "true");
1718
+ this._position();
1719
+ this._addRepositionListeners();
1720
+ if (this._openedWithKeyboard) this._focusItem(0);
1721
+ this.dispatchEvent(new CustomEvent("open", { bubbles: true }));
1722
+ }
1723
+ _close() {
1724
+ if (!this._isOpen || !this._menu || !this._trigger) return;
1725
+ if (this._isControlled) {
1726
+ this.dispatchEvent(new CustomEvent("close-request", { bubbles: true }));
1727
+ return;
1728
+ }
1729
+ this._doClose();
1730
+ }
1731
+ _doClose() {
1732
+ if (!this._isOpen || !this._menu || !this._trigger) return;
1733
+ this._closeAllSubmenus();
1734
+ this.removeAttribute("data-open");
1735
+ this.removeAttribute("open");
1736
+ this._menu.hidden = true;
1737
+ this._trigger.setAttribute("aria-expanded", "false");
1738
+ this._items.forEach((item) => {
1739
+ item.setAttribute("tabindex", "-1");
1740
+ item.removeAttribute("data-active");
1741
+ });
1742
+ this._activeIndex = -1;
1743
+ this._removeRepositionListeners();
1744
+ this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
1745
+ }
1746
+ _toggleOpen() {
1747
+ if (this._isOpen) this._close();
1748
+ else this._open();
1749
+ }
1750
+ _closeAll() {
1751
+ let root = this.parentElement?.closest("ix-dropdown");
1752
+ if (!root) {
1753
+ this._close();
1754
+ this._trigger?.focus();
1755
+ return;
1756
+ }
1757
+ while (root.parentElement?.closest("ix-dropdown")) root = root.parentElement.closest("ix-dropdown");
1758
+ root._close();
1759
+ root._trigger?.focus();
1760
+ }
1761
+ _closeAllSubmenus() {
1762
+ (this._menu?.querySelectorAll("ix-dropdown[data-submenu]"))?.forEach((submenu) => {
1763
+ if (submenu._isOpen) submenu._close();
1764
+ });
1765
+ }
1766
+ _position() {
1767
+ if (!this._menu || !this._trigger) return;
1768
+ const triggerRect = this._trigger.getBoundingClientRect();
1769
+ const menuRect = this._menu.getBoundingClientRect();
1770
+ const vw = window.innerWidth;
1771
+ const vh = window.innerHeight;
1772
+ const gap = 2;
1773
+ let top;
1774
+ let left;
1775
+ if (this._isSubmenu) {
1776
+ const parentMenuRect = this.parentElement?.getBoundingClientRect();
1777
+ const anchorRight = parentMenuRect?.right ?? triggerRect.right;
1778
+ const anchorLeft = parentMenuRect?.left ?? triggerRect.left;
1779
+ const spaceRight = vw - anchorRight;
1780
+ const spaceLeft = anchorLeft;
1781
+ if (spaceRight >= menuRect.width || spaceRight >= spaceLeft) left = anchorRight + gap;
1782
+ else left = anchorLeft - menuRect.width - gap;
1783
+ top = triggerRect.top;
1784
+ if (top + menuRect.height > vh - gap) top = Math.max(gap, vh - menuRect.height - gap);
1785
+ } else {
1786
+ const [vertical, horizontal] = this._placement.split("-");
1787
+ const spaceBelow = vh - triggerRect.bottom;
1788
+ const spaceAbove = triggerRect.top;
1789
+ if (vertical === "bottom" && spaceBelow >= menuRect.height + gap) top = triggerRect.bottom + gap;
1790
+ else if (vertical === "top" && spaceAbove >= menuRect.height + gap) top = triggerRect.top - menuRect.height - gap;
1791
+ else if (spaceBelow >= spaceAbove) top = triggerRect.bottom + gap;
1792
+ else top = triggerRect.top - menuRect.height - gap;
1793
+ if (horizontal === "end") left = triggerRect.right - menuRect.width;
1794
+ else left = triggerRect.left;
1795
+ if (left + menuRect.width > vw - gap) left = vw - menuRect.width - gap;
1796
+ if (left < gap) left = gap;
1797
+ }
1798
+ if (top + menuRect.height > vh - gap) top = Math.max(gap, vh - menuRect.height - gap);
1799
+ if (top < gap) top = gap;
1800
+ this._menu.style.top = `${top}px`;
1801
+ this._menu.style.left = `${left}px`;
1802
+ }
1803
+ _addRepositionListeners() {
1804
+ if (this._onReposition) return;
1805
+ this._onReposition = () => {
1806
+ if (this._isOpen) this._position();
1807
+ };
1808
+ window.addEventListener("scroll", this._onReposition, true);
1809
+ window.addEventListener("resize", this._onReposition);
1810
+ }
1811
+ _removeRepositionListeners() {
1812
+ if (this._onReposition) {
1813
+ window.removeEventListener("scroll", this._onReposition, true);
1814
+ window.removeEventListener("resize", this._onReposition);
1815
+ this._onReposition = null;
1816
+ }
1817
+ }
1818
+ };
1819
+ //#endregion
1324
1820
  //#region lib/components/field/IxField.ts
1325
1821
  /**
1326
1822
  * IxField — tilgjengelighetslim mellom label, input og hjelpetekster.
@@ -2493,9 +2989,10 @@ var IxPhoneNumberField = class extends HTMLElement {
2493
2989
  const prop = flag === "readonly" ? "readOnly" : flag;
2494
2990
  const read = () => mode === "attribute" ? control.hasAttribute(flag) : !!control[prop];
2495
2991
  const write = (value) => {
2496
- if (mode === "attribute") if (value) control.setAttribute(flag, "");
2497
- else control.removeAttribute(flag);
2498
- else control[prop] = value;
2992
+ if (mode === "attribute") {
2993
+ if (value) control.setAttribute(flag, "");
2994
+ else control.removeAttribute(flag);
2995
+ } else control[prop] = value;
2499
2996
  };
2500
2997
  if (groupOn) {
2501
2998
  if (!ownStore.has(control)) ownStore.set(control, read());
@@ -2597,6 +3094,317 @@ var IxPhoneNumberField = class extends HTMLElement {
2597
3094
  }
2598
3095
  };
2599
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
2600
3408
  //#region lib/utils/platform.ts
2601
3409
  /**
2602
3410
  * Plattform-deteksjon for web components.
@@ -3503,7 +4311,9 @@ customElements.define("ix-radio-group", IxRadioGroup);
3503
4311
  customElements.define("ix-checkbox-group", IxCheckboxGroup);
3504
4312
  customElements.define("ix-combobox", IxCombobox);
3505
4313
  customElements.define("ix-date-field", IxDateField);
4314
+ customElements.define("ix-dropdown", IxDropdown);
3506
4315
  customElements.define("ix-phone-number-field", IxPhoneNumberField);
4316
+ customElements.define("ix-popover", IxPopover);
3507
4317
  customElements.define("ix-progress-bar", IxProgressBar);
3508
4318
  customElements.define("ix-tabs", IxTabs);
3509
4319
  customElements.define("ix-tab-list", IxTabList);
@@ -3511,4 +4321,4 @@ customElements.define("ix-tab", IxTab);
3511
4321
  customElements.define("ix-tab-panel", IxTabPanel);
3512
4322
  globalThis.cn = cn;
3513
4323
  //#endregion
3514
- export { BUILTIN_FORMAT_NAMES, COMMON_ICON_NAMES, IxCheckboxGroup, IxCombobox, IxDateField, 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 };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * IxDropdown — ARIA-lim for en nedtrekksmeny med handlinger eller navigasjon.
3
+ *
4
+ * ## Designfilosofi
5
+ *
6
+ * Komponenten kobler en trigger-knapp med en meny. Forfatteren skriver en
7
+ * <button> (trigger) og en .ix-dropdown__menu med .ix-dropdown__item-elementer
8
+ * i light DOM. Komponentens jobb er å sette opp ARIA-attributter, håndtere
9
+ * tastaturnavigasjon, posisjonere menyen, og styre åpen/lukket-tilstand.
10
+ *
11
+ * ## Roving tabindex
12
+ *
13
+ * Alle items har tabindex="-1" unntatt det aktive (fokuserte). Piltaster
14
+ * flytter fokus mellom items. Dette følger APG Menu Button-mønsteret.
15
+ *
16
+ * ## Submenyer
17
+ *
18
+ * En submenu er en nøstet <ix-dropdown data-submenu> inne i parent-menyen.
19
+ * ArrowRight på en submenu-trigger åpner submenyen, ArrowLeft lukker den.
20
+ * Submenyen posisjoneres til høyre for parent-item (flipper ved plassmangel).
21
+ *
22
+ * ## Posisjonering
23
+ *
24
+ * Menyen posisjoneres manuelt med getBoundingClientRect + viewport-clamp
25
+ * (samme grep som tooltip og combobox) — position: fixed, JS setter top/left.
26
+ *
27
+ * @example
28
+ * <ix-dropdown>
29
+ * <button class="ix-button" data-variant="secondary">Handlinger</button>
30
+ * <div class="ix-dropdown__menu">
31
+ * <button class="ix-dropdown__item">Rediger</button>
32
+ * <button class="ix-dropdown__item">Dupliser</button>
33
+ * <hr class="ix-dropdown__divider" />
34
+ * <button class="ix-dropdown__item" data-danger>Slett</button>
35
+ * </div>
36
+ * </ix-dropdown>
37
+ */
38
+ export declare class IxDropdown extends HTMLElement {
39
+ private _instanceId;
40
+ private _trigger;
41
+ private _menu;
42
+ private _items;
43
+ private _activeIndex;
44
+ private _onTriggerClick;
45
+ private _onTriggerKeydown;
46
+ private _onMenuKeydown;
47
+ private _onMenuClick;
48
+ private _onMenuPointerenter;
49
+ private _onDocPointer;
50
+ private _onFocusout;
51
+ private _onReposition;
52
+ private _itemObserver;
53
+ private _typeaheadBuffer;
54
+ private _typeaheadTimer;
55
+ private _openedWithKeyboard;
56
+ static get observedAttributes(): string[];
57
+ /**
58
+ * Kontrollert modus: når `data-controlled` er satt, endrer ikke komponenten
59
+ * egen åpen-tilstand. Den dispatcher kun `open-request` / `close-request`,
60
+ * og foreldren må sette `open`-attributtet for å faktisk åpne/lukke.
61
+ */
62
+ private get _isControlled();
63
+ connectedCallback(): void;
64
+ disconnectedCallback(): void;
65
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
66
+ get open(): boolean;
67
+ set open(value: boolean);
68
+ private get _isOpen();
69
+ private get _isSubmenu();
70
+ private get _placement();
71
+ private _wire;
72
+ private _wireItems;
73
+ private _rewireItems;
74
+ private _handleTriggerKeydown;
75
+ private _handleMenuKeydown;
76
+ private _handleItemPointerEnter;
77
+ private _closeOtherSubmenus;
78
+ private _handleItemClick;
79
+ private _handleTypeahead;
80
+ private _focusItem;
81
+ private _focusNextItem;
82
+ private _open;
83
+ private _doOpen;
84
+ private _close;
85
+ private _doClose;
86
+ private _toggleOpen;
87
+ private _closeAll;
88
+ private _closeAllSubmenus;
89
+ private _position;
90
+ private _addRepositionListeners;
91
+ private _removeRepositionListeners;
92
+ }
@@ -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.18.1",
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
  },