@vialiq/web-components 0.17.0 → 0.19.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.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * OverlayManagerService
3
+ *
4
+ * A singleton service responsible for managing the z-index stacking context
5
+ * of all floating elements (Modals, Dropdowns, Tooltips, Toasts) across the application.
6
+ *
7
+ * It ensures that newly opened overlays always appear on top of existing ones by
8
+ * maintaining a registry and dynamically calculating the next highest z-index.
9
+ * It also manages global state side-effects, such as locking `document.body` scroll
10
+ * when a modal is active.
11
+ */ class OverlayManagerService {
12
+ getBaseZIndex(element) {
13
+ if (typeof document !== 'undefined' && typeof getComputedStyle !== 'undefined') {
14
+ // Derive base stacking context from the modal z-index token (minus 10 to start slightly below it)
15
+ // Read directly from the element if provided to support CSS variable scoping/theming.
16
+ const target = element || document.documentElement;
17
+ const cssVar = getComputedStyle(target).getPropertyValue('--vi-modal-z-index').trim();
18
+ const parsed = parseInt(cssVar, 10);
19
+ return !isNaN(parsed) ? parsed - 10 : 1040;
20
+ }
21
+ return 1040;
22
+ }
23
+ overlays = [];
24
+ _previousOverflow = null;
25
+ _previousPaddingRight = null;
26
+ _inertedElements = [];
27
+ /**
28
+ * Registers an element as an active overlay.
29
+ * Calculates and returns the appropriate z-index for this overlay.
30
+ *
31
+ * @param element The DOM element being registered (e.g., the modal dialog or dropdown listbox)
32
+ * @param type The type of overlay, used to determine behaviors like scroll-locking.
33
+ * @param scrollStrategy How this overlay interacts with background scrolling.
34
+ * @returns The calculated z-index to be applied to the element.
35
+ */ register(element, type = 'dropdown', scrollStrategy, options) {
36
+ this.unregister(element); // Ensure no duplicates
37
+ let highestZIndex = this.getBaseZIndex(element);
38
+ if (this.overlays.length > 0) {
39
+ highestZIndex = Math.max(...this.overlays.map((o)=>o.zIndex), highestZIndex);
40
+ }
41
+ // Increment by 10 to allow room for backdrops (which typically sit at z-index - 1)
42
+ const newZIndex = highestZIndex + 10;
43
+ const finalStrategy = scrollStrategy ?? (type === 'modal' ? 'block' : 'noop');
44
+ this.overlays.push({
45
+ element,
46
+ type,
47
+ zIndex: newZIndex,
48
+ scrollStrategy: finalStrategy,
49
+ noBackdrop: options?.noBackdrop
50
+ });
51
+ this._updateBodyScroll();
52
+ this._syncInertState();
53
+ return newZIndex;
54
+ }
55
+ /**
56
+ * Unregisters an element, removing it from the overlay stack.
57
+ * Should be called when the overlay is closed or disconnected from the DOM.
58
+ *
59
+ * @param element The DOM element to unregister.
60
+ */ unregister(element) {
61
+ this.overlays = this.overlays.filter((o)=>o.element !== element);
62
+ this._updateBodyScroll();
63
+ this._syncInertState();
64
+ }
65
+ /**
66
+ * Gets the assigned z-index for an element if it is currently registered.
67
+ *
68
+ * @param element The DOM element to query.
69
+ * @returns The z-index number, or null if the element is not registered.
70
+ */ getZIndex(element) {
71
+ const item = this.overlays.find((o)=>o.element === element);
72
+ return item ? item.zIndex : null;
73
+ }
74
+ /**
75
+ * Evaluates whether the provided element is currently the top-most active overlay.
76
+ * Useful for trapping focus or handling global Escape key presses.
77
+ *
78
+ * @param element The DOM element to check.
79
+ * @returns True if the element has the highest z-index in the registry.
80
+ */ isTopOverlay(element) {
81
+ if (this.overlays.length === 0) return false;
82
+ const topOverlay = this.overlays.reduce((prev, current)=>prev.zIndex > current.zIndex ? prev : current);
83
+ return topOverlay.element === element;
84
+ }
85
+ /**
86
+ * Syncs the `inert` attribute on `document.body` children based on the active overlay stack.
87
+ * Modals with a backdrop trap focus globally, so everything beneath them must be `inert`.
88
+ */ _syncInertState() {
89
+ if (typeof document === 'undefined') return;
90
+ // Find the topmost modal that requires a backdrop
91
+ const blockingOverlays = this.overlays.filter((o)=>o.type === 'modal' && !o.noBackdrop);
92
+ const topBlocking = blockingOverlays.length > 0 ? blockingOverlays[blockingOverlays.length - 1] : null;
93
+ if (!topBlocking) {
94
+ // If no blocking overlays, clear all inert state
95
+ this._inertedElements.forEach((el)=>{
96
+ el.inert = false;
97
+ });
98
+ this._inertedElements = [];
99
+ return;
100
+ }
101
+ // Determine which overlays should NOT be inert (the top blocking one and any above it)
102
+ const activeOverlayElements = new Set();
103
+ const topBlockingIndex = this.overlays.indexOf(topBlocking);
104
+ for(let i = topBlockingIndex; i < this.overlays.length; i++){
105
+ activeOverlayElements.add(this.overlays[i].element);
106
+ }
107
+ // Mark children of body
108
+ Array.from(document.body.children).forEach((child)=>{
109
+ const el = child;
110
+ // Never make the active overlays inert
111
+ if (activeOverlayElements.has(el)) {
112
+ if (this._inertedElements.includes(el)) {
113
+ el.inert = false;
114
+ this._inertedElements = this._inertedElements.filter((e)=>e !== el);
115
+ }
116
+ return;
117
+ }
118
+ // If it's not an active overlay, and not already inert, make it inert
119
+ if (!el.inert) {
120
+ el.inert = true;
121
+ this._inertedElements.push(el);
122
+ }
123
+ });
124
+ }
125
+ /**
126
+ * Locks or unlocks the document.body scroll based on the active overlays.
127
+ * Modals (and other overlays with scrollStrategy='block') require the body
128
+ * to be unscrollable. It applies a utility class `vi-scroll-locked` to the body.
129
+ */ _updateBodyScroll() {
130
+ const hasBlock = this.overlays.some((o)=>o.scrollStrategy === 'block');
131
+ if (hasBlock) {
132
+ // Prevent double-setting if already locked
133
+ if (!document.body.classList.contains('vi-scroll-locked')) {
134
+ // Calculate scrollbar width before removing overflow
135
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
136
+ document.body.classList.add('vi-scroll-locked');
137
+ this._previousOverflow = document.body.style.getPropertyValue('overflow') || null;
138
+ this._previousPaddingRight = document.body.style.getPropertyValue('padding-right') || null;
139
+ document.body.style.setProperty('overflow', 'hidden', 'important');
140
+ // Apply compensation padding to prevent layout shift
141
+ if (scrollbarWidth > 0) {
142
+ const currentPadding = parseFloat(window.getComputedStyle(document.body).paddingRight || '0');
143
+ document.body.style.setProperty('padding-right', `${currentPadding + scrollbarWidth}px`, 'important');
144
+ }
145
+ }
146
+ } else {
147
+ if (document.body.classList.contains('vi-scroll-locked')) {
148
+ document.body.classList.remove('vi-scroll-locked');
149
+ if (this._previousOverflow !== null) {
150
+ document.body.style.setProperty('overflow', this._previousOverflow);
151
+ } else {
152
+ document.body.style.removeProperty('overflow');
153
+ }
154
+ if (this._previousPaddingRight !== null) {
155
+ document.body.style.setProperty('padding-right', this._previousPaddingRight);
156
+ } else {
157
+ document.body.style.removeProperty('padding-right');
158
+ }
159
+ this._previousOverflow = null;
160
+ this._previousPaddingRight = null;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ // Export as a singleton
166
+ const OverlayManager = new OverlayManagerService();
167
+
168
+ export { OverlayManager as O };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vialiq/web-components",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -103,7 +103,7 @@
103
103
  },
104
104
  "peerDependencies": {
105
105
  "@floating-ui/dom": ">=1.0.0",
106
- "@vialiq/flux-ui": "^0.14.0",
106
+ "@vialiq/flux-ui": "^0.15.0",
107
107
  "@vialiq/icons": ">=0.0.3 <0.1.0",
108
108
  "lit": "^3.0.0"
109
109
  },
@@ -2,7 +2,7 @@ import { unsafeCSS, css, nothing, html } from 'lit';
2
2
  import { customElement, property, state, query } from 'lit/decorators.js';
3
3
  import { V as ViElement } from '../vi-element-C6GfDPs3.js';
4
4
  import { autoUpdate, offset, flip, shift, arrow, computePosition } from '@floating-ui/dom';
5
- import { O as OverlayManager } from '../overlay-manager-CZSDXl6Z.js';
5
+ import { O as OverlayManager } from '../overlay-manager-Ch_6fr_D.js';
6
6
 
7
7
  const tooltipStyles = "@charset \"UTF-8\";@layer reset,components,utilities;.tooltip-panel{position:fixed;z-index:var(--vi-tooltip-z-index, var(--vi-tooltip-z-index, 1070));pointer-events:none;opacity:0;transform:scale(.95);transition:opacity .15s ease-out,transform .15s ease-out;margin:0;border:none;background:transparent;padding:0;overflow:visible;font-family:var(--vi-font-family-base, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif)}.tooltip-panel[popover]:popover-open{display:block;pointer-events:auto;opacity:1;transform:scale(1)}.tooltip-panel{transition-property:opacity,transform,display,overlay;transition-behavior:allow-discrete}@media(prefers-reduced-motion:reduce){.tooltip-panel{transition:none;transform:none}}.tooltip-content{background-color:var(--vi-tooltip-background, var(--vi-layer-inverse, #111827));color:var(--vi-tooltip-color, var(--vi-text-primary-inverse, #ffffff));font-size:var(--vi-tooltip-font-size, 12px);line-height:1.4;padding:var(--vi-tooltip-padding, 6px 10px);border-radius:var(--vi-tooltip-border-radius, 4px);box-shadow:var(--vi-tooltip-shadow, var(--vi-shadow-md, 0 4px 6px -1px rgba(0, 0, 0, .1)));max-width:var(--vi-tooltip-max-width, 240px);word-wrap:break-word;white-space:normal;position:relative}.tooltip-arrow{position:absolute;width:0;height:0;border-style:solid;border-color:transparent;pointer-events:none}.tooltip-panel[placement^=top] .tooltip-arrow,.tooltip-panel[data-placement^=top] .tooltip-arrow{bottom:calc(-1 * var(--vi-tooltip-arrow-size, 6px));left:50%;transform:translate(-50%);border-width:var(--vi-tooltip-arrow-size, 6px) var(--vi-tooltip-arrow-size, 6px) 0;border-top-color:var(--vi-tooltip-background, var(--vi-layer-inverse, #111827))}.tooltip-panel[placement^=bottom] .tooltip-arrow,.tooltip-panel[data-placement^=bottom] .tooltip-arrow{top:calc(-1 * var(--vi-tooltip-arrow-size, 6px));left:50%;transform:translate(-50%);border-width:0 var(--vi-tooltip-arrow-size, 6px) var(--vi-tooltip-arrow-size, 6px);border-bottom-color:var(--vi-tooltip-background, var(--vi-layer-inverse, #111827))}.tooltip-panel[placement=left] .tooltip-arrow,.tooltip-panel[data-placement=left] .tooltip-arrow{right:calc(-1 * var(--vi-tooltip-arrow-size, 6px));top:50%;transform:translateY(-50%);border-width:var(--vi-tooltip-arrow-size, 6px) 0 var(--vi-tooltip-arrow-size, 6px) var(--vi-tooltip-arrow-size, 6px);border-left-color:var(--vi-tooltip-background, var(--vi-layer-inverse, #111827))}.tooltip-panel[placement=right] .tooltip-arrow,.tooltip-panel[data-placement=right] .tooltip-arrow{left:calc(-1 * var(--vi-tooltip-arrow-size, 6px));top:50%;transform:translateY(-50%);border-width:var(--vi-tooltip-arrow-size, 6px) var(--vi-tooltip-arrow-size, 6px) var(--vi-tooltip-arrow-size, 6px) 0;border-right-color:var(--vi-tooltip-background, var(--vi-layer-inverse, #111827))}.tooltip-panel[placement$=-start] .tooltip-arrow,.tooltip-panel[data-placement$=-start] .tooltip-arrow{left:12px;transform:none}.tooltip-panel[placement$=-end] .tooltip-arrow,.tooltip-panel[data-placement$=-end] .tooltip-arrow{left:auto;right:12px;transform:none}:host{display:inline-block}";
8
8
 
@@ -1,108 +0,0 @@
1
- /**
2
- * OverlayManagerService
3
- *
4
- * A singleton service responsible for managing the z-index stacking context
5
- * of all floating elements (Modals, Dropdowns, Tooltips, Toasts) across the application.
6
- *
7
- * It ensures that newly opened overlays always appear on top of existing ones by
8
- * maintaining a registry and dynamically calculating the next highest z-index.
9
- * It also manages global state side-effects, such as locking `document.body` scroll
10
- * when a modal is active.
11
- */ class OverlayManagerService {
12
- _baseZIndexCache;
13
- get baseZIndex() {
14
- if (this._baseZIndexCache !== undefined) return this._baseZIndexCache;
15
- if (typeof document !== 'undefined' && typeof getComputedStyle !== 'undefined') {
16
- // Derive base stacking context from the modal z-index token (minus 10 to start slightly below it)
17
- const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--vi-modal-z-index').trim();
18
- const parsed = parseInt(cssVar, 10);
19
- this._baseZIndexCache = !isNaN(parsed) ? parsed - 10 : 1040;
20
- } else {
21
- this._baseZIndexCache = 1040;
22
- }
23
- return this._baseZIndexCache;
24
- }
25
- overlays = [];
26
- _previousOverflow = null;
27
- /**
28
- * Registers an element as an active overlay.
29
- * Calculates and returns the appropriate z-index for this overlay.
30
- *
31
- * @param element The DOM element being registered (e.g., the modal dialog or dropdown listbox)
32
- * @param type The type of overlay, used to determine behaviors like scroll-locking.
33
- * @returns The calculated z-index to be applied to the element.
34
- */ register(element, type = 'dropdown') {
35
- this.unregister(element); // Ensure no duplicates
36
- let highestZIndex = this.baseZIndex;
37
- if (this.overlays.length > 0) {
38
- highestZIndex = Math.max(...this.overlays.map((o)=>o.zIndex));
39
- }
40
- // Increment by 10 to allow room for backdrops (which typically sit at z-index - 1)
41
- const newZIndex = highestZIndex + 10;
42
- this.overlays.push({
43
- element,
44
- type,
45
- zIndex: newZIndex
46
- });
47
- this._updateBodyScroll();
48
- return newZIndex;
49
- }
50
- /**
51
- * Unregisters an element, removing it from the overlay stack.
52
- * Should be called when the overlay is closed or disconnected from the DOM.
53
- *
54
- * @param element The DOM element to unregister.
55
- */ unregister(element) {
56
- this.overlays = this.overlays.filter((o)=>o.element !== element);
57
- this._updateBodyScroll();
58
- }
59
- /**
60
- * Gets the assigned z-index for an element if it is currently registered.
61
- *
62
- * @param element The DOM element to query.
63
- * @returns The z-index number, or null if the element is not registered.
64
- */ getZIndex(element) {
65
- const item = this.overlays.find((o)=>o.element === element);
66
- return item ? item.zIndex : null;
67
- }
68
- /**
69
- * Evaluates whether the provided element is currently the top-most active overlay.
70
- * Useful for trapping focus or handling global Escape key presses.
71
- *
72
- * @param element The DOM element to check.
73
- * @returns True if the element has the highest z-index in the registry.
74
- */ isTopOverlay(element) {
75
- if (this.overlays.length === 0) return false;
76
- const topOverlay = this.overlays.reduce((prev, current)=>prev.zIndex > current.zIndex ? prev : current);
77
- return topOverlay.element === element;
78
- }
79
- /**
80
- * Locks or unlocks the document.body scroll based on the presence of modals.
81
- * Modals require the body to be unscrollable to trap scroll inside the modal.
82
- * It applies a utility class `vi-scroll-locked` to the body.
83
- */ _updateBodyScroll() {
84
- const hasModal = this.overlays.some((o)=>o.type === 'modal');
85
- if (hasModal) {
86
- // Prevent double-setting if already locked
87
- if (!document.body.classList.contains('vi-scroll-locked')) {
88
- document.body.classList.add('vi-scroll-locked');
89
- this._previousOverflow = document.body.style.getPropertyValue('overflow') || null;
90
- document.body.style.setProperty('overflow', 'hidden', 'important');
91
- }
92
- } else {
93
- if (document.body.classList.contains('vi-scroll-locked')) {
94
- document.body.classList.remove('vi-scroll-locked');
95
- if (this._previousOverflow !== null) {
96
- document.body.style.setProperty('overflow', this._previousOverflow);
97
- } else {
98
- document.body.style.removeProperty('overflow');
99
- }
100
- this._previousOverflow = null;
101
- }
102
- }
103
- }
104
- }
105
- // Export as a singleton
106
- const OverlayManager = new OverlayManagerService();
107
-
108
- export { OverlayManager as O };