@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
|
package/dist/npm/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
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")
|
|
2984
|
-
|
|
2985
|
-
|
|
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.
|
|
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
|
|
43
|
+
"@types/node": "^26.2.0",
|
|
44
44
|
"jsdom": "^29.1.1",
|
|
45
45
|
"typescript": "~6.0.3",
|
|
46
|
-
"vite": "^8.1
|
|
46
|
+
"vite": "^8.2.1",
|
|
47
47
|
"vite-plugin-dts": "^5.0.3",
|
|
48
48
|
"vitest": "^4.1.10"
|
|
49
49
|
},
|