@aquera/nile-elements 1.2.8-beta-1.7 → 1.2.8-beta-1.9

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.
Files changed (52) hide show
  1. package/demo/index.html +54 -51
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +285 -274
  5. package/dist/nile-auto-complete/index.cjs.js +1 -1
  6. package/dist/nile-auto-complete/index.esm.js +1 -1
  7. package/dist/nile-auto-complete/nile-auto-complete.cjs.js +1 -1
  8. package/dist/nile-auto-complete/nile-auto-complete.cjs.js.map +1 -1
  9. package/dist/nile-auto-complete/nile-auto-complete.esm.js +17 -13
  10. package/dist/nile-auto-complete/nile-auto-complete.test.cjs.js +1 -1
  11. package/dist/nile-auto-complete/nile-auto-complete.test.cjs.js.map +1 -1
  12. package/dist/nile-auto-complete/nile-auto-complete.test.esm.js +1 -1
  13. package/dist/nile-auto-complete/portal-manager.cjs.js +2 -0
  14. package/dist/nile-auto-complete/portal-manager.cjs.js.map +1 -0
  15. package/dist/nile-auto-complete/portal-manager.esm.js +1 -0
  16. package/dist/nile-auto-complete/portal-utils.cjs.js +2 -0
  17. package/dist/nile-auto-complete/portal-utils.cjs.js.map +1 -0
  18. package/dist/nile-auto-complete/portal-utils.esm.js +1 -0
  19. package/dist/nile-chip/index.cjs.js +1 -1
  20. package/dist/nile-chip/index.esm.js +1 -1
  21. package/dist/nile-chip/nile-chip.cjs.js +1 -1
  22. package/dist/nile-chip/nile-chip.cjs.js.map +1 -1
  23. package/dist/nile-chip/nile-chip.esm.js +14 -7
  24. package/dist/nile-chip/nile-chip.test.cjs.js +1 -1
  25. package/dist/nile-chip/nile-chip.test.cjs.js.map +1 -1
  26. package/dist/nile-chip/nile-chip.test.esm.js +1 -1
  27. package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js +1 -1
  28. package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js.map +1 -1
  29. package/dist/nile-lite-tooltip/nile-lite-tooltip.esm.js +1 -1
  30. package/dist/src/nile-auto-complete/nile-auto-complete.d.ts +18 -1
  31. package/dist/src/nile-auto-complete/nile-auto-complete.js +131 -9
  32. package/dist/src/nile-auto-complete/nile-auto-complete.js.map +1 -1
  33. package/dist/src/nile-auto-complete/portal-manager.d.ts +41 -0
  34. package/dist/src/nile-auto-complete/portal-manager.js +308 -0
  35. package/dist/src/nile-auto-complete/portal-manager.js.map +1 -0
  36. package/dist/src/nile-auto-complete/portal-utils.d.ts +31 -0
  37. package/dist/src/nile-auto-complete/portal-utils.js +166 -0
  38. package/dist/src/nile-auto-complete/portal-utils.js.map +1 -0
  39. package/dist/src/nile-chip/nile-chip.d.ts +7 -0
  40. package/dist/src/nile-chip/nile-chip.js +53 -10
  41. package/dist/src/nile-chip/nile-chip.js.map +1 -1
  42. package/dist/src/nile-lite-tooltip/nile-lite-tooltip.js.map +1 -1
  43. package/dist/src/version.js +2 -2
  44. package/dist/src/version.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +1 -1
  47. package/src/nile-auto-complete/nile-auto-complete.ts +148 -13
  48. package/src/nile-auto-complete/portal-manager.ts +410 -0
  49. package/src/nile-auto-complete/portal-utils.ts +221 -0
  50. package/src/nile-chip/nile-chip.ts +54 -12
  51. package/src/nile-lite-tooltip/nile-lite-tooltip.ts +4 -3
  52. package/vscode-html-custom-data.json +17 -2
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Webcomponent nile-elements following open-wc recommendations",
4
4
  "license": "MIT",
5
5
  "author": "nile-elements",
6
- "version": "1.2.8-beta-1.7",
6
+ "version": "1.2.8-beta-1.9",
7
7
  "main": "dist/src/index.js",
8
8
  "type": "module",
9
9
  "module": "dist/src/index.js",
@@ -10,8 +10,12 @@ import { styles } from './nile-auto-complete.css';
10
10
  import NileElement from '../internal/nile-element';
11
11
  import type { CSSResultGroup, PropertyValues } from 'lit';
12
12
  import { NileDropdown } from '../nile-dropdown';
13
+ import { watch } from '../internal/watch';
14
+ import { AutoCompletePortalManager } from './portal-manager';
15
+ import { NileInput } from '../nile-input';
13
16
 
14
17
  import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
18
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
15
19
 
16
20
  // Define the custom element 'nile-auto-complete'
17
21
  @customElement('nile-auto-complete')
@@ -21,12 +25,22 @@ export class NileAutoComplete extends NileElement {
21
25
 
22
26
  @query('nile-dropdown') dropdownElement: NileDropdown;
23
27
 
28
+ @query('nile-input') inputElement: NileInput;
29
+
24
30
  // Define component properties
25
31
 
26
32
  @property({ type: Boolean }) disabled: boolean = false;
27
33
 
28
34
  @property({ type: Boolean }) isDropdownOpen: boolean = false;
29
35
 
36
+ /**
37
+ * When true, the dropdown menu will be appended to the document body instead of the parent container.
38
+ * This is useful when the parent has overflow: hidden, clip-path, or transform applied.
39
+ */
40
+ @property({ type: Boolean, reflect: true }) portal = false;
41
+
42
+ private readonly portalManager = new AutoCompletePortalManager(this);
43
+
30
44
  @property({ type: Boolean }) enableVirtualScroll: boolean = false;
31
45
 
32
46
  @property({ type: Boolean }) openOnFocus: boolean = false;
@@ -54,6 +68,16 @@ export class NileAutoComplete extends NileElement {
54
68
  connectedCallback() {
55
69
  super.connectedCallback();
56
70
  this.renderItemFunction=(item:any)=>item;
71
+ this.handleDocumentFocusIn = this.handleDocumentFocusIn.bind(this);
72
+ this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
73
+ this.handleWindowResize = this.handleWindowResize.bind(this);
74
+ this.handleWindowScroll = this.handleWindowScroll.bind(this);
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ super.disconnectedCallback();
79
+ this.removeOpenListeners();
80
+ this.portalManager.cleanupPortalAppend();
57
81
  }
58
82
 
59
83
  protected updated(changedProperties: PropertyValues): void {
@@ -61,13 +85,102 @@ export class NileAutoComplete extends NileElement {
61
85
  if (changedProperties.has('allMenuItems')){
62
86
  this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
63
87
  this.setVirtualMenuWidth();
88
+ if (this.portal && this.isDropdownOpen) {
89
+ this.portalManager.updatePortalOptions();
90
+ }
91
+ }
92
+ if (changedProperties.has('isDropdownOpen')) {
93
+ this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
94
+ this.handleDropdownOpenChange();
64
95
  }
65
- if (changedProperties.has('isDropdownOpen')) this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
66
96
  if (changedProperties.has('value')){
67
97
  this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
98
+ if (this.portal && this.isDropdownOpen) {
99
+ this.portalManager.updatePortalOptions();
100
+ }
101
+ }
102
+ if (changedProperties.has('portal')) {
103
+ this.handlePortalChange();
68
104
  }
69
105
  }
70
106
 
107
+ @watch('portal', { waitUntilFirstUpdate: true })
108
+ handlePortalChange(): void {
109
+ if (this.isDropdownOpen) {
110
+ if (this.portal) {
111
+ this.portalManager.setupPortalAppend();
112
+ } else {
113
+ this.portalManager.cleanupPortalAppend();
114
+ }
115
+ }
116
+ }
117
+
118
+ private handleDropdownOpenChange(): void {
119
+ if (this.isDropdownOpen) {
120
+ this.addOpenListeners();
121
+ if (this.portal) {
122
+ this.portalManager.setupPortalAppend();
123
+ }
124
+ } else {
125
+ this.removeOpenListeners();
126
+ if (this.portal) {
127
+ this.portalManager.cleanupPortalAppend();
128
+ }
129
+ }
130
+ }
131
+
132
+ private addOpenListeners(): void {
133
+ document.addEventListener('focusin', this.handleDocumentFocusIn);
134
+ document.addEventListener('mousedown', this.handleDocumentMouseDown);
135
+
136
+ if (this.portal) {
137
+ window.addEventListener('resize', this.handleWindowResize);
138
+ window.addEventListener('scroll', this.handleWindowScroll, true);
139
+ }
140
+ }
141
+
142
+ private removeOpenListeners(): void {
143
+ document.removeEventListener('focusin', this.handleDocumentFocusIn);
144
+ document.removeEventListener('mousedown', this.handleDocumentMouseDown);
145
+ window.removeEventListener('resize', this.handleWindowResize);
146
+ window.removeEventListener('scroll', this.handleWindowScroll, true);
147
+ }
148
+
149
+ private handleDocumentFocusIn(event: FocusEvent) {
150
+ if (!this.isDropdownOpen) return;
151
+ const path = event.composedPath();
152
+ const hitSelf = path.includes(this);
153
+ const hitDropdown = this.dropdownElement && path.includes(this.dropdownElement);
154
+ const hitPortalAppend = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
155
+
156
+ if (!hitSelf && !hitDropdown && !hitPortalAppend) {
157
+ this.isDropdownOpen = false;
158
+ this.dropdownElement?.hide();
159
+ }
160
+ }
161
+
162
+ private handleDocumentMouseDown(event: MouseEvent) {
163
+ if (!this.isDropdownOpen) return;
164
+
165
+ const path = event.composedPath();
166
+ const hitSelf = path.includes(this);
167
+ const hitDropdown = this.dropdownElement && path.includes(this.dropdownElement);
168
+ const hitPortalAppend = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
169
+
170
+ if (!hitSelf && !hitDropdown && !hitPortalAppend) {
171
+ this.isDropdownOpen = false;
172
+ this.dropdownElement?.hide();
173
+ }
174
+ }
175
+
176
+ private handleWindowResize = (): void => {
177
+ this.portalManager.updatePortalAppendPosition();
178
+ };
179
+
180
+ private handleWindowScroll = (): void => {
181
+ this.portalManager.updatePortalAppendPosition();
182
+ };
183
+
71
184
  public render(): TemplateResult {
72
185
  const content=this.enableVirtualScroll?this.getVirtualizedContent():this.getContent();
73
186
  return html`
@@ -96,7 +209,7 @@ export class NileAutoComplete extends NileElement {
96
209
 
97
210
  getVirtualizedContent():TemplateResult{
98
211
  return html`
99
- <nile-menu class="virtualized__menu" @nile-select=${this.handleSelect} id="content-menu" exportparts="menu__items-wrapper:options__wrapper">
212
+ <nile-menu class="virtualized__menu" @nile-select=${this.handleSelect} id="content-menu" exportparts="menu__items-wrapper:options__wrapper" style=${this.portal ? 'display: none;' : ''}>
100
213
  ${virtualize({
101
214
  items: this.menuItems,
102
215
  renderItem: (item:any):TemplateResult=>this.getItemRenderFunction(item),
@@ -108,21 +221,34 @@ export class NileAutoComplete extends NileElement {
108
221
 
109
222
  getContent():TemplateResult{
110
223
  return html`
111
- <nile-menu id="content-menu" @nile-select=${this.handleSelect} exportparts="menu__items-wrapper:options__wrapper">
224
+ <nile-menu id="content-menu" @nile-select=${this.handleSelect} exportparts="menu__items-wrapper:options__wrapper" style=${this.portal ? 'display: none;' : ''}>
112
225
  ${this.menuItems.map((item: any) => this.getItemRenderFunction(item))}
113
226
  </nile-menu>`
114
227
  }
115
228
 
116
- getItemRenderFunction(item:any):TemplateResult{
117
- const value=this.renderItemFunction(item)
118
- return html`
119
- <nile-menu-item value=${value}>
120
- ${value}
121
- </nile-menu-item>
122
- `;
123
- }
229
+ getItemRenderFunction(item: any): TemplateResult {
230
+ const value = this.renderItemFunction(item);
231
+ const hasTooltip = !!item.tooltip;
232
+ const shouldShowTooltip =
233
+ hasTooltip && (!item.tooltip.for || item.tooltip.for === 'menu');
234
+ const tooltipContent = hasTooltip ? item.tooltip.content : value;
235
+
236
+ return shouldShowTooltip
237
+ ? html`
238
+ <nile-menu-item value=${value}>
239
+ <nile-lite-tooltip allowHTML .content=${tooltipContent}>
240
+ <span class="menu-item-text">${unsafeHTML(value)}</span>
241
+ </nile-lite-tooltip>
242
+ </nile-menu-item>
243
+ `
244
+ : html`
245
+ <nile-menu-item value=${value}>${unsafeHTML(value)}</nile-menu-item>
246
+ `;
247
+ }
248
+
249
+
124
250
 
125
- private handleSelect(event: CustomEvent) {
251
+ handleSelect(event: CustomEvent) {
126
252
  this.value = event.detail.value;
127
253
  this.emit('nile-complete', { value: event.detail.value });
128
254
  this.isDropdownOpen = false;
@@ -148,7 +274,12 @@ export class NileAutoComplete extends NileElement {
148
274
  this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
149
275
 
150
276
  this.isDropdownOpen = this.menuItems.length > 0;
151
- if (this.isDropdownOpen) this.dropdownElement?.show();
277
+ if (this.isDropdownOpen) {
278
+ this.dropdownElement?.show();
279
+ if (this.portal) {
280
+ this.portalManager.updatePortalOptions();
281
+ }
282
+ }
152
283
  }
153
284
 
154
285
  public handleFocus() {
@@ -156,6 +287,10 @@ export class NileAutoComplete extends NileElement {
156
287
  return;
157
288
  }
158
289
 
290
+ if(this.portal) {
291
+ this.inputElement?.focus();
292
+ }
293
+
159
294
  // Delay opening the dropdown to allow focus to take effect
160
295
  setTimeout(() => {
161
296
  this.isDropdownOpen = true;
@@ -0,0 +1,410 @@
1
+ import {
2
+ autoUpdate,
3
+ computePosition,
4
+ flip,
5
+ offset,
6
+ shift,
7
+ size,
8
+ platform,
9
+ type Placement,
10
+ type MiddlewareData,
11
+ type ComputePositionConfig
12
+ } from '@floating-ui/dom';
13
+ import { PortalUtils, PortalContentUtils, PortalEventUtils } from './portal-utils';
14
+
15
+ export class AutoCompletePortalManager {
16
+ private portalContainer: HTMLElement | null = null;
17
+ private originalMenuParent: HTMLElement | null = null;
18
+ private measuredMenuHeight: number | null = null;
19
+ private component: any;
20
+ private clonedMenu: HTMLElement | null = null;
21
+ private cleanupAutoUpdate: (() => void) | null = null;
22
+ private currentPlacement: Placement = 'bottom';
23
+ private currentMiddlewareData: MiddlewareData | null = null;
24
+
25
+ constructor(component: any) {
26
+ this.component = component;
27
+ }
28
+
29
+ private createPortalAppendContainer(): HTMLElement {
30
+ const container = document.createElement('div');
31
+ container.style.position = 'absolute';
32
+ container.style.zIndex = '9999';
33
+ container.style.pointerEvents = 'none';
34
+ container.style.width = 'auto';
35
+ container.style.minWidth = 'auto';
36
+ container.className = 'nile-auto-complete-portal-append';
37
+ return container;
38
+ }
39
+
40
+ positionPortalAppend(): void {
41
+ if (!this.portalContainer || !this.component.dropdownElement) return;
42
+
43
+ this.measureMenuHeight();
44
+ this.computeFloatingUIPosition();
45
+ }
46
+
47
+ private measureMenuHeight(): void {
48
+ if (this.measuredMenuHeight || !this.portalContainer) return;
49
+
50
+ this.portalContainer.style.position = 'absolute';
51
+ this.portalContainer.style.visibility = 'hidden';
52
+ this.portalContainer.style.top = '0px';
53
+ this.portalContainer.style.left = '0px';
54
+
55
+ this.portalContainer.offsetHeight;
56
+
57
+ this.measuredMenuHeight = this.portalContainer.offsetHeight;
58
+
59
+ this.portalContainer.style.visibility = '';
60
+ }
61
+
62
+ private async computeFloatingUIPosition(): Promise<void> {
63
+ if (!this.portalContainer) return;
64
+
65
+ const referenceElement = this.component.inputElement || this.component;
66
+ const floatingElement = this.portalContainer;
67
+
68
+ try {
69
+ const { x, y, placement, middlewareData } = await this.calculateFloatingUIPosition(
70
+ referenceElement,
71
+ floatingElement
72
+ );
73
+
74
+ this.applyFloatingUIPosition(floatingElement, referenceElement, x, y, placement, middlewareData);
75
+
76
+ } catch (error) {
77
+ console.warn('Floating UI positioning failed, falling back to simple positioning:', error);
78
+ this.fallbackPositioning();
79
+ }
80
+ }
81
+
82
+ private async calculateFloatingUIPosition(
83
+ referenceElement: HTMLElement,
84
+ floatingElement: HTMLElement
85
+ ): Promise<{ x: number; y: number; placement: Placement; middlewareData: MiddlewareData }> {
86
+ const boundary = PortalUtils.findBoundaryElements(referenceElement);
87
+ // Use 'bottom-start' or 'top-start' to align left edges for auto-width menu
88
+ const basePlacement = PortalUtils.getOptimalPlacement(referenceElement);
89
+ const initialPlacement = basePlacement === 'top' ? 'top-start' : 'bottom-start';
90
+ const middleware = this.createFloatingUIMiddleware(boundary);
91
+
92
+ return await computePosition(referenceElement, floatingElement, {
93
+ placement: initialPlacement,
94
+ strategy: 'fixed',
95
+ middleware,
96
+ platform: this.createCustomPlatform()
97
+ });
98
+ }
99
+
100
+ private createFloatingUIMiddleware(boundary: Element[] | undefined): ComputePositionConfig['middleware'] {
101
+ return [
102
+ offset(4),
103
+ size({
104
+ apply: this.handleSizeMiddleware.bind(this),
105
+ padding: 10,
106
+ boundary: boundary
107
+ }),
108
+ flip({
109
+ fallbackPlacements: ['bottom-start', 'top-start', 'bottom', 'top', 'bottom-end', 'top-end'],
110
+ fallbackStrategy: 'bestFit',
111
+ padding: 10,
112
+ boundary: boundary
113
+ }),
114
+ shift({
115
+ padding: 10,
116
+ crossAxis: true,
117
+ boundary: boundary
118
+ })
119
+ ];
120
+ }
121
+
122
+ private handleSizeMiddleware({ availableWidth, availableHeight, elements, rects }: {
123
+ availableWidth: number;
124
+ availableHeight: number;
125
+ elements: { floating: HTMLElement };
126
+ rects: { reference: { x: number; y: number; width: number; height: number } };
127
+ }): void {
128
+ const maxHeight = PortalUtils.calculateOptimalHeight(
129
+ rects.reference,
130
+ window.innerHeight,
131
+ this.currentPlacement
132
+ );
133
+
134
+ // elements.floating.style.maxWidth = `${availableWidth}px`;
135
+ elements.floating.style.maxHeight = `${maxHeight}px`;
136
+
137
+ elements.floating.style.setProperty('--auto-size-available-width', `${availableWidth}px`);
138
+ elements.floating.style.setProperty('--auto-size-available-height', `${maxHeight}px`);
139
+ }
140
+
141
+ private createCustomPlatform() {
142
+ return platform;
143
+ }
144
+
145
+ private applyFloatingUIPosition(
146
+ floatingElement: HTMLElement,
147
+ referenceElement: HTMLElement,
148
+ x: number,
149
+ y: number,
150
+ placement: Placement,
151
+ middlewareData: MiddlewareData
152
+ ): void {
153
+ // For auto-complete, align left edge with input element
154
+ // Use reference element's left position, not Floating UI's x (which might shift)
155
+ const referenceRect = referenceElement.getBoundingClientRect();
156
+
157
+ Object.assign(floatingElement.style, {
158
+ left: `${referenceRect.left}px`,
159
+ top: `${y}px`,
160
+ position: 'fixed',
161
+ pointerEvents: 'auto',
162
+ width: 'auto',
163
+ minWidth: 'auto'
164
+ });
165
+
166
+ this.currentPlacement = placement;
167
+ this.currentMiddlewareData = middlewareData;
168
+
169
+ PortalUtils.applyCollisionData(floatingElement, middlewareData, placement);
170
+
171
+ const placementClass = placement.split('-')[0];
172
+ floatingElement.className = `nile-auto-complete-portal-append menu__listbox--${placementClass}`;
173
+ }
174
+
175
+ private fallbackPositioning(): void {
176
+ if (!this.portalContainer) return;
177
+
178
+ const referenceElement = this.component.inputElement || this.component;
179
+ const rect = referenceElement.getBoundingClientRect();
180
+ const viewportHeight = window.innerHeight;
181
+ const menuHeight = this.measuredMenuHeight || 200;
182
+
183
+ const spaceBelow = viewportHeight - rect.bottom;
184
+ const spaceAbove = rect.top;
185
+
186
+ let topPosition: number;
187
+ let placementClass: string;
188
+ let maxHeight: number;
189
+
190
+ if (spaceAbove > spaceBelow) {
191
+ maxHeight = Math.max(spaceAbove - 20, 100);
192
+ topPosition = Math.max(rect.top - maxHeight - 4, 10);
193
+ placementClass = 'top';
194
+ } else {
195
+ maxHeight = Math.max(spaceBelow - 20, 100);
196
+ topPosition = rect.bottom + 4;
197
+ placementClass = 'bottom';
198
+ }
199
+
200
+ this.portalContainer.style.left = `${rect.left}px`;
201
+ this.portalContainer.style.top = `${topPosition}px`;
202
+ // Let the menu auto-size based on its content, don't force width to match input
203
+ this.portalContainer.style.width = 'auto';
204
+ this.portalContainer.style.minWidth = 'auto';
205
+ this.portalContainer.style.maxHeight = `${maxHeight}px`;
206
+ this.portalContainer.style.pointerEvents = 'auto';
207
+ this.portalContainer.className = `nile-auto-complete-portal-append menu__listbox--${placementClass}`;
208
+
209
+ this.calculateAndSetAutoSizeProperties(rect, topPosition, placementClass);
210
+ }
211
+
212
+ private calculateAndSetAutoSizeProperties(rect: DOMRect, topPosition: number, placementClass: string): void {
213
+ if (!this.portalContainer) return;
214
+
215
+ const viewportHeight = window.innerHeight;
216
+ const viewportWidth = window.innerWidth;
217
+
218
+ let availableHeight: number;
219
+ if (placementClass === 'top') {
220
+ availableHeight = rect.top - 10;
221
+ } else {
222
+ availableHeight = viewportHeight - rect.bottom - 10;
223
+ }
224
+
225
+ const availableWidth = Math.min(rect.width, viewportWidth - rect.left - 10);
226
+
227
+ this.portalContainer.style.setProperty('--auto-size-available-height', `${Math.max(availableHeight, 100)}px`);
228
+ this.portalContainer.style.setProperty('--auto-size-available-width', `${Math.max(availableWidth, 200)}px`);
229
+ }
230
+
231
+ updatePortalAppendPosition(): void {
232
+ if (this.component.portal && this.portalContainer) {
233
+ this.positionPortalAppend();
234
+ }
235
+ }
236
+
237
+ handleWindowResize(): void {
238
+ if (this.component.portal && this.portalContainer) {
239
+ this.positionPortalAppend();
240
+ }
241
+ }
242
+
243
+ private setupAutoUpdatePositioning(): void {
244
+ if (!this.portalContainer || !this.component) return;
245
+
246
+ this.cleanupAutoUpdatePositioning();
247
+
248
+ this.cleanupAutoUpdate = autoUpdate(
249
+ this.component,
250
+ this.portalContainer,
251
+ () => {
252
+ this.computeFloatingUIPosition();
253
+ },
254
+ {
255
+ ancestorScroll: true,
256
+ ancestorResize: true,
257
+ elementResize: true,
258
+ layoutShift: true,
259
+ animationFrame: true
260
+ }
261
+ );
262
+ }
263
+
264
+ private cleanupAutoUpdatePositioning(): void {
265
+ if (this.cleanupAutoUpdate) {
266
+ this.cleanupAutoUpdate();
267
+ this.cleanupAutoUpdate = null;
268
+ }
269
+ }
270
+
271
+ private injectStylesToDocument(): void {
272
+ if (!this.portalContainer) return;
273
+
274
+ const styleId = PortalUtils.generateStyleId();
275
+
276
+ if (document.getElementById(styleId)) return;
277
+
278
+ const componentStyles = (this.component.constructor as any).styles;
279
+ if (!componentStyles) return;
280
+
281
+ const styleElement = document.createElement('style');
282
+ styleElement.id = styleId;
283
+ styleElement.textContent = PortalUtils.extractStylesAsCSS(componentStyles);
284
+
285
+ document.head.appendChild(styleElement);
286
+
287
+ (this.portalContainer as any).__injectedStyleId = styleId;
288
+ }
289
+
290
+ private adoptStylesToPortalAppend(): void {
291
+ if (!this.portalContainer) return;
292
+ this.injectStylesToDocument();
293
+ }
294
+
295
+ setupPortalAppend(): void {
296
+ if (!this.component.portal) return;
297
+
298
+ this.component.updateComplete.then(() => {
299
+ setTimeout(() => {
300
+ // Try to find menu in shadow root first, then fallback to querySelector
301
+ const menu = this.component.shadowRoot?.querySelector('#content-menu') as HTMLElement ||
302
+ this.component.querySelector('#content-menu') as HTMLElement;
303
+ if (menu && this.component.isDropdownOpen) {
304
+ this.originalMenuParent = menu.parentElement as HTMLElement;
305
+
306
+ this.clonedMenu = this.createPortalMenu();
307
+
308
+ this.portalContainer = this.createPortalAppendContainer();
309
+ this.portalContainer.appendChild(this.clonedMenu);
310
+ document.body.appendChild(this.portalContainer);
311
+
312
+ this.adoptStylesToPortalAppend();
313
+
314
+ this.clonedMenu.style.display = '';
315
+ this.positionPortalAppend();
316
+
317
+ this.setupPortalEventListeners();
318
+
319
+ this.setupAutoUpdatePositioning();
320
+
321
+ window.addEventListener('resize', this.handleWindowResize.bind(this));
322
+ }
323
+ }, 10);
324
+ });
325
+ }
326
+
327
+ private createPortalMenu(): HTMLElement {
328
+ return PortalContentUtils.createPortalMenu(this.component);
329
+ }
330
+
331
+ private setupPortalEventListeners(): void {
332
+ PortalEventUtils.setupPortalEventListeners(this.clonedMenu!, this.component);
333
+ }
334
+
335
+ cleanupPortalAppend(): void {
336
+ this.cleanupAutoUpdatePositioning();
337
+
338
+ if (this.portalContainer && this.portalContainer.parentNode) {
339
+ const injectedStyleId = (this.portalContainer as any).__injectedStyleId;
340
+ if (injectedStyleId) {
341
+ const styleElement = document.getElementById(injectedStyleId);
342
+ if (styleElement) {
343
+ styleElement.remove();
344
+ }
345
+ }
346
+
347
+ this.portalContainer.parentNode.removeChild(this.portalContainer);
348
+ }
349
+
350
+ window.removeEventListener('resize', this.handleWindowResize.bind(this));
351
+
352
+ this.portalContainer = null;
353
+ this.originalMenuParent = null;
354
+ this.clonedMenu = null;
355
+ this.measuredMenuHeight = null;
356
+ this.currentPlacement = 'bottom';
357
+ this.currentMiddlewareData = null;
358
+ }
359
+
360
+ get portalContainerElement(): HTMLElement | null {
361
+ return this.portalContainer;
362
+ }
363
+
364
+ resetMeasuredHeight(): void {
365
+ this.measuredMenuHeight = null;
366
+ }
367
+
368
+ updatePortalOptions(): void {
369
+ if (this.portalContainer && this.clonedMenu) {
370
+ PortalContentUtils.updatePortalMenuItems(this.clonedMenu, this.component);
371
+ this.forceReposition();
372
+ }
373
+ }
374
+
375
+ forceReposition(): void {
376
+ if (this.portalContainer) {
377
+ this.computeFloatingUIPosition();
378
+ }
379
+ }
380
+
381
+ getCurrentPlacement(): Placement {
382
+ return this.currentPlacement;
383
+ }
384
+
385
+ getCurrentMiddlewareData(): MiddlewareData | null {
386
+ return this.currentMiddlewareData;
387
+ }
388
+
389
+ isUsingFloatingUI(): boolean {
390
+ return this.cleanupAutoUpdate !== null;
391
+ }
392
+
393
+ isPositioningOptimal(): boolean {
394
+ if (!this.portalContainer || !this.currentMiddlewareData) return true;
395
+
396
+ const referenceElement = this.component.inputElement || this.component;
397
+ const rect = referenceElement.getBoundingClientRect();
398
+ const viewportHeight = window.innerHeight;
399
+ const spaceBelow = viewportHeight - rect.bottom;
400
+ const spaceAbove = rect.top;
401
+
402
+ const isAbove = this.currentPlacement.startsWith('top');
403
+ const isBelow = this.currentPlacement.startsWith('bottom');
404
+
405
+ if (isAbove && spaceBelow > spaceAbove) return false;
406
+ if (isBelow && spaceAbove > spaceBelow) return false;
407
+
408
+ return true;
409
+ }
410
+ }