wally-ui 1.12.1 → 1.13.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/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/playground/showcase/src/app/app.routes.server.ts +4 -0
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +164 -31
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +25 -3
- package/playground/showcase/src/app/components/ai/ai-prompt-input/ai-prompt-input.html +1 -1
- package/playground/showcase/src/app/components/badge/badge.css +0 -0
- package/playground/showcase/src/app/components/badge/badge.html +3 -0
- package/playground/showcase/src/app/components/badge/badge.ts +24 -0
- package/playground/showcase/src/app/components/button/button.html +1 -3
- package/playground/showcase/src/app/components/button/button.ts +4 -4
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.html +9 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-content/dropdown-menu-content.ts +167 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.html +5 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-group/dropdown-menu-group.ts +10 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.html +6 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-item/dropdown-menu-item.ts +37 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.html +3 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-label/dropdown-menu-label.ts +11 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.html +1 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-portal/dropdown-menu-portal.ts +11 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.html +1 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-separator/dropdown-menu-separator.ts +11 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.html +3 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub/dropdown-menu-sub.ts +16 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.html +9 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.ts +31 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.html +13 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-trigger/dropdown-menu-sub-trigger.ts +40 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.spec.ts +16 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub.service.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.html +8 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.spec.ts +23 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-trigger/dropdown-menu-trigger.ts +55 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.css +0 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.html +3 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.spec.ts +16 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.service.ts +31 -0
- package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu.ts +69 -0
- package/playground/showcase/src/app/components/tooltip/tooltip.ts +195 -80
- package/playground/showcase/src/app/pages/documentation/components/components.html +110 -51
- package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.css +1 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.examples.ts +404 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.html +612 -0
- package/playground/showcase/src/app/pages/documentation/components/dropdown-menu-docs/dropdown-menu-docs.ts +127 -0
- package/playground/showcase/src/app/pages/home/home.html +10 -6
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Component, effect, ElementRef, input, OnDestroy } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import { DropdownMenuService } from './dropdown-menu.service';
|
|
4
|
+
|
|
5
|
+
export type TriggerMode = 'click' | 'hover';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'wally-dropdown-menu',
|
|
9
|
+
imports: [],
|
|
10
|
+
providers: [DropdownMenuService],
|
|
11
|
+
templateUrl: './dropdown-menu.html',
|
|
12
|
+
styleUrl: './dropdown-menu.css'
|
|
13
|
+
})
|
|
14
|
+
export class DropdownMenu implements OnDestroy {
|
|
15
|
+
triggerMode = input<TriggerMode>('click');
|
|
16
|
+
|
|
17
|
+
private clickOutsideListener: ((event: MouseEvent) => void) | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public dropdownMenuService: DropdownMenuService,
|
|
21
|
+
private elementRef: ElementRef
|
|
22
|
+
) {
|
|
23
|
+
effect(() => {
|
|
24
|
+
const mode = this.triggerMode();
|
|
25
|
+
this.dropdownMenuService.triggerMode.set(mode);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
effect(() => {
|
|
29
|
+
if (this.dropdownMenuService.isOpen()) {
|
|
30
|
+
this.addClickOutsideListener();
|
|
31
|
+
} else {
|
|
32
|
+
this.removeClickOutsideListener();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ngOnDestroy(): void {
|
|
38
|
+
this.removeClickOutsideListener();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Adds a click listener to the document to detect clicks outside the dropdown.
|
|
43
|
+
* Uses setTimeout to prevent the same click that opens the dropdown from immediately closing it.
|
|
44
|
+
*/
|
|
45
|
+
private addClickOutsideListener(): void {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
this.clickOutsideListener = (event: MouseEvent) => {
|
|
48
|
+
const clickedElement = event.target as HTMLElement;
|
|
49
|
+
const dropdownElement = this.elementRef.nativeElement as HTMLElement;
|
|
50
|
+
|
|
51
|
+
if (!dropdownElement.contains(clickedElement)) {
|
|
52
|
+
this.dropdownMenuService.close();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
document.addEventListener('click', this.clickOutsideListener);
|
|
57
|
+
}, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Removes the click outside listener from the document and cleans up the reference.
|
|
62
|
+
*/
|
|
63
|
+
private removeClickOutsideListener(): void {
|
|
64
|
+
if (this.clickOutsideListener) {
|
|
65
|
+
document.removeEventListener('click', this.clickOutsideListener);
|
|
66
|
+
this.clickOutsideListener = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, computed,
|
|
1
|
+
import { Component, computed, ElementRef, input, InputSignal, OnDestroy, signal, ViewChild, WritableSignal } from '@angular/core';
|
|
2
2
|
import { CommonModule } from '@angular/common';
|
|
3
3
|
|
|
4
4
|
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right' | 'auto';
|
|
@@ -19,40 +19,34 @@ export class Tooltip implements OnDestroy {
|
|
|
19
19
|
offset: InputSignal<number> = input<number>(2);
|
|
20
20
|
|
|
21
21
|
visible: WritableSignal<boolean> = signal<boolean>(false);
|
|
22
|
-
actualPosition = signal<Exclude<TooltipPosition, 'auto'>>('top');
|
|
23
|
-
tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
|
22
|
+
actualPosition: WritableSignal<"top" | "bottom" | "left" | "right"> = signal<Exclude<TooltipPosition, 'auto'>>('top');
|
|
23
|
+
tooltipId: string = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
|
24
|
+
isPositioned: WritableSignal<boolean> = signal<boolean>(false);
|
|
24
25
|
|
|
25
26
|
private showTimeout?: number;
|
|
26
27
|
private hideTimeout?: number;
|
|
27
28
|
|
|
28
29
|
tooltipClasses = computed(() => {
|
|
29
|
-
const base = 'absolute z-
|
|
30
|
+
const base = 'absolute z-10 py-1.5 px-3 text-sm text-white font-medium bg-[#121212] shadow-lg whitespace-nowrap pointer-events-none rounded-xl';
|
|
30
31
|
const darkMode = 'dark:bg-white dark:text-[#121212]';
|
|
31
32
|
|
|
32
33
|
const positions = {
|
|
33
|
-
top:
|
|
34
|
-
bottom:
|
|
35
|
-
left:
|
|
36
|
-
right:
|
|
34
|
+
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
|
35
|
+
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
|
36
|
+
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
|
37
|
+
right: 'left-full top-1/2 -translate-y-1/2 ml-2'
|
|
37
38
|
};
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
const animation = this.visible()
|
|
40
|
+
const animation = this.visible() && this.isPositioned()
|
|
41
41
|
? 'transition-opacity duration-150 opacity-100'
|
|
42
|
-
: '
|
|
42
|
+
: 'opacity-0';
|
|
43
43
|
|
|
44
44
|
return `${base} ${darkMode} ${positions[this.actualPosition()]} ${animation}`;
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
constructor(
|
|
48
48
|
private elementRef: ElementRef
|
|
49
|
-
) {
|
|
50
|
-
effect(() => {
|
|
51
|
-
if (this.visible() && this.position() === 'auto') {
|
|
52
|
-
setTimeout(() => this.calculateBestPosition(), 10);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
49
|
+
) { }
|
|
56
50
|
|
|
57
51
|
ngOnDestroy() {
|
|
58
52
|
if (this.showTimeout) {
|
|
@@ -74,11 +68,18 @@ export class Tooltip implements OnDestroy {
|
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
this.showTimeout = window.setTimeout(() => {
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
// Set initial position
|
|
79
72
|
if (this.position() !== 'auto') {
|
|
80
73
|
this.actualPosition.set(this.position() as Exclude<TooltipPosition, 'auto'>);
|
|
81
74
|
}
|
|
75
|
+
|
|
76
|
+
// Make tooltip visible with opacity 0 to measure it
|
|
77
|
+
this.visible.set(true);
|
|
78
|
+
|
|
79
|
+
// Calculate best position before showing (next frame after DOM update)
|
|
80
|
+
requestAnimationFrame(() => {
|
|
81
|
+
this.calculateBestPosition();
|
|
82
|
+
});
|
|
82
83
|
}, this.delay());
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -90,100 +91,214 @@ export class Tooltip implements OnDestroy {
|
|
|
90
91
|
|
|
91
92
|
this.hideTimeout = window.setTimeout(() => {
|
|
92
93
|
this.visible.set(false);
|
|
94
|
+
this.isPositioned.set(false); // Reset positioning flag
|
|
93
95
|
}, 100);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Calculates the best position for the tooltip based on available viewport space.
|
|
100
|
+
*
|
|
101
|
+
* This method intelligently positions the tooltip to ensure it remains fully visible
|
|
102
|
+
* within the viewport boundaries. It respects the user's preferred position but will
|
|
103
|
+
* automatically switch to an alternative position if the preferred one doesn't fit.
|
|
104
|
+
*
|
|
105
|
+
* **Algorithm Steps:**
|
|
106
|
+
* 1. Measures available space in all four directions (top, bottom, left, right)
|
|
107
|
+
* 2. Validates each position considering:
|
|
108
|
+
* - Tooltip dimensions (width/height)
|
|
109
|
+
* - Viewport boundaries
|
|
110
|
+
* - Required offset and padding (8px each)
|
|
111
|
+
* 3. Determines priority order:
|
|
112
|
+
* - If `position="auto"`: tries top → bottom → right → left
|
|
113
|
+
* - If specific position: tries that position first, then fallback to others
|
|
114
|
+
* 4. Selects the first valid position from priority order
|
|
115
|
+
* 5. If no position is fully valid, selects the one with most available space
|
|
116
|
+
* 6. Applies fine-tuning adjustments:
|
|
117
|
+
* - For top/bottom: shifts horizontally if edges overflow
|
|
118
|
+
* - For left/right: shifts vertically if edges overflow
|
|
119
|
+
*
|
|
120
|
+
* **Position Validation:**
|
|
121
|
+
* - **Top/Bottom**: Checks if there's enough vertical space AND tooltip fits horizontally
|
|
122
|
+
* - **Left/Right**: Checks if there's enough horizontal space AND tooltip fits vertically
|
|
123
|
+
*
|
|
124
|
+
* **Viewport Adjustment:**
|
|
125
|
+
* - Uses `calc()` to adjust `left` or `top` CSS properties
|
|
126
|
+
* - Preserves Tailwind's `-translate-x-1/2` and `-translate-y-1/2` transforms
|
|
127
|
+
* - Ensures 8px minimum padding from all viewport edges
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* // User specifies position="bottom"
|
|
131
|
+
* // If bottom doesn't fit → tries top → right → left
|
|
132
|
+
* // Applies horizontal shift if tooltip overflows left/right edges
|
|
133
|
+
*
|
|
134
|
+
* @returns void
|
|
135
|
+
*/
|
|
96
136
|
private calculateBestPosition() {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const rect = hostElement.getBoundingClientRect();
|
|
102
|
-
const tooltipEl = this.tooltipElement?.nativeElement;
|
|
103
|
-
if (!tooltipEl) return;
|
|
104
|
-
|
|
105
|
-
const tooltipRect = tooltipEl.getBoundingClientRect();
|
|
106
|
-
const offset = this.offset();
|
|
107
|
-
|
|
108
|
-
const viewportWidth = window.innerWidth;
|
|
109
|
-
const viewportHeight = window.innerHeight;
|
|
110
|
-
|
|
111
|
-
const spaceAbove = rect.top;
|
|
112
|
-
const spaceBelow = viewportHeight - rect.bottom;
|
|
113
|
-
const spaceLeft = rect.left;
|
|
114
|
-
const spaceRight = viewportWidth - rect.right;
|
|
137
|
+
const wrapperElement = this.elementRef.nativeElement.querySelector('.tooltip-wrapper');
|
|
138
|
+
if (!wrapperElement) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
115
141
|
|
|
116
|
-
const
|
|
117
|
-
|
|
142
|
+
const tooltipElement = this.tooltipElement?.nativeElement;
|
|
143
|
+
if (!tooltipElement) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
118
146
|
|
|
119
|
-
const
|
|
120
|
-
|
|
147
|
+
const wrapperBounds = wrapperElement.getBoundingClientRect();
|
|
148
|
+
const tooltipBounds = tooltipElement.getBoundingClientRect();
|
|
149
|
+
|
|
150
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
151
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
152
|
+
|
|
153
|
+
// Available space in each direction from the wrapper element
|
|
154
|
+
const availableSpaceAbove = wrapperBounds.top;
|
|
155
|
+
const availableSpaceBelow = viewportHeight - wrapperBounds.bottom;
|
|
156
|
+
const availableSpaceLeft = wrapperBounds.left;
|
|
157
|
+
const availableSpaceRight = viewportWidth - wrapperBounds.right;
|
|
158
|
+
|
|
159
|
+
// Actual tooltip dimensions
|
|
160
|
+
const tooltipWidth = tooltipBounds.width;
|
|
161
|
+
const tooltipHeight = tooltipBounds.height;
|
|
162
|
+
|
|
163
|
+
// Fixed spacing values (8px each = 0.5rem in Tailwind)
|
|
164
|
+
const offsetFromWrapper = 8; // Gap between tooltip and wrapper
|
|
165
|
+
const edgePadding = 8; // Minimum distance from viewport edges
|
|
166
|
+
|
|
167
|
+
// Calculate horizontal center position of the wrapper
|
|
168
|
+
const wrapperCenterX = wrapperBounds.left + wrapperBounds.width / 2;
|
|
169
|
+
const wrapperCenterY = wrapperBounds.top + wrapperBounds.height / 2;
|
|
170
|
+
|
|
171
|
+
// Calculate where tooltip edges would be when centered
|
|
172
|
+
const tooltipLeftEdgeWhenCentered = wrapperCenterX - tooltipWidth / 2;
|
|
173
|
+
const tooltipRightEdgeWhenCentered = wrapperCenterX + tooltipWidth / 2;
|
|
174
|
+
const tooltipTopEdgeWhenCentered = wrapperCenterY - tooltipHeight / 2;
|
|
175
|
+
const tooltipBottomEdgeWhenCentered = wrapperCenterY + tooltipHeight / 2;
|
|
176
|
+
|
|
177
|
+
// Check if tooltip fits within viewport for each position
|
|
178
|
+
const positionValidations: {
|
|
179
|
+
position: Exclude<TooltipPosition, 'auto'>;
|
|
180
|
+
availableSpace: number;
|
|
181
|
+
fitsInViewport: boolean;
|
|
182
|
+
fitsHorizontally?: boolean;
|
|
183
|
+
fitsVertically?: boolean;
|
|
184
|
+
}[] = [
|
|
121
185
|
{
|
|
122
186
|
position: 'top',
|
|
123
|
-
|
|
124
|
-
|
|
187
|
+
availableSpace: availableSpaceAbove,
|
|
188
|
+
fitsInViewport: availableSpaceAbove >= tooltipHeight + offsetFromWrapper + edgePadding,
|
|
189
|
+
fitsHorizontally: tooltipLeftEdgeWhenCentered >= edgePadding &&
|
|
190
|
+
tooltipRightEdgeWhenCentered <= viewportWidth - edgePadding
|
|
125
191
|
},
|
|
126
192
|
{
|
|
127
193
|
position: 'bottom',
|
|
128
|
-
|
|
129
|
-
|
|
194
|
+
availableSpace: availableSpaceBelow,
|
|
195
|
+
fitsInViewport: availableSpaceBelow >= tooltipHeight + offsetFromWrapper + edgePadding,
|
|
196
|
+
fitsHorizontally: tooltipLeftEdgeWhenCentered >= edgePadding &&
|
|
197
|
+
tooltipRightEdgeWhenCentered <= viewportWidth - edgePadding
|
|
130
198
|
},
|
|
131
199
|
{
|
|
132
200
|
position: 'left',
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
availableSpace: availableSpaceLeft,
|
|
202
|
+
fitsInViewport: availableSpaceLeft >= tooltipWidth + offsetFromWrapper + edgePadding,
|
|
203
|
+
fitsVertically: tooltipTopEdgeWhenCentered >= edgePadding &&
|
|
204
|
+
tooltipBottomEdgeWhenCentered <= viewportHeight - edgePadding
|
|
135
205
|
},
|
|
136
206
|
{
|
|
137
207
|
position: 'right',
|
|
138
|
-
|
|
139
|
-
|
|
208
|
+
availableSpace: availableSpaceRight,
|
|
209
|
+
fitsInViewport: availableSpaceRight >= tooltipWidth + offsetFromWrapper + edgePadding,
|
|
210
|
+
fitsVertically: tooltipTopEdgeWhenCentered >= edgePadding &&
|
|
211
|
+
tooltipBottomEdgeWhenCentered <= viewportHeight - edgePadding
|
|
140
212
|
}
|
|
141
213
|
];
|
|
142
214
|
|
|
143
|
-
|
|
215
|
+
// Determine which positions to try, in order of preference
|
|
216
|
+
let positionsToTryInOrder: Exclude<TooltipPosition, 'auto'>[];
|
|
144
217
|
|
|
145
|
-
|
|
218
|
+
if (this.position() === 'auto') {
|
|
219
|
+
// Auto mode: use default priority order
|
|
220
|
+
positionsToTryInOrder = ['top', 'bottom', 'right', 'left'];
|
|
221
|
+
} else {
|
|
222
|
+
// Specific position: try user's preference first, then fallback to others
|
|
223
|
+
const userPreferredPosition = this.position() as Exclude<TooltipPosition, 'auto'>;
|
|
224
|
+
const allOtherPositions = (['top', 'bottom', 'right', 'left'] as Exclude<TooltipPosition, 'auto'>[])
|
|
225
|
+
.filter(position => position !== userPreferredPosition);
|
|
146
226
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
227
|
+
positionsToTryInOrder = [userPreferredPosition, ...allOtherPositions];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Find the first position that fits completely in the viewport
|
|
231
|
+
let finalSelectedPosition: Exclude<TooltipPosition, 'auto'> = positionsToTryInOrder[0];
|
|
232
|
+
|
|
233
|
+
for (const candidatePosition of positionsToTryInOrder) {
|
|
234
|
+
const validation = positionValidations.find(v => v.position === candidatePosition);
|
|
235
|
+
if (validation?.fitsInViewport) {
|
|
236
|
+
finalSelectedPosition = candidatePosition;
|
|
151
237
|
break;
|
|
152
238
|
}
|
|
153
239
|
}
|
|
154
240
|
|
|
155
|
-
if
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
241
|
+
// Fallback: if no position fits, use the one with most available space
|
|
242
|
+
const currentPositionValidation = positionValidations.find(v => v.position === finalSelectedPosition);
|
|
243
|
+
if (!currentPositionValidation?.fitsInViewport) {
|
|
244
|
+
const positionWithMostSpace = positionValidations.reduce((best, current) =>
|
|
245
|
+
current.availableSpace > best.availableSpace ? current : best,
|
|
246
|
+
positionValidations[0]
|
|
247
|
+
);
|
|
248
|
+
finalSelectedPosition = positionWithMostSpace.position;
|
|
160
249
|
}
|
|
161
250
|
|
|
162
|
-
this.actualPosition.set(
|
|
251
|
+
this.actualPosition.set(finalSelectedPosition);
|
|
252
|
+
|
|
253
|
+
// Apply fine-tuning adjustments after position is set
|
|
254
|
+
requestAnimationFrame(() => {
|
|
255
|
+
// Mark as positioned to trigger fade-in animation
|
|
256
|
+
this.isPositioned.set(true);
|
|
163
257
|
|
|
164
|
-
setTimeout(() => {
|
|
165
|
-
const updatedTooltipRect = tooltipEl.getBoundingClientRect();
|
|
166
|
-
const padding = 8;
|
|
167
258
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
259
|
+
const tooltipBoundsAfterPositioning = tooltipElement.getBoundingClientRect();
|
|
260
|
+
|
|
261
|
+
// Reset any previous adjustments
|
|
262
|
+
tooltipElement.style.left = '';
|
|
263
|
+
tooltipElement.style.top = '';
|
|
264
|
+
|
|
265
|
+
// Adjust horizontal position for top/bottom tooltips that overflow edges
|
|
266
|
+
if (finalSelectedPosition === 'top' || finalSelectedPosition === 'bottom') {
|
|
267
|
+
let horizontalAdjustment = 0;
|
|
268
|
+
|
|
269
|
+
// Check if tooltip overflows left edge
|
|
270
|
+
if (tooltipBoundsAfterPositioning.left < edgePadding) {
|
|
271
|
+
horizontalAdjustment = edgePadding - tooltipBoundsAfterPositioning.left;
|
|
272
|
+
}
|
|
273
|
+
// Check if tooltip overflows right edge
|
|
274
|
+
else if (tooltipBoundsAfterPositioning.right > viewportWidth - edgePadding) {
|
|
275
|
+
horizontalAdjustment = (viewportWidth - edgePadding) - tooltipBoundsAfterPositioning.right;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (horizontalAdjustment !== 0) {
|
|
279
|
+
// Shift tooltip while preserving centered alignment
|
|
280
|
+
tooltipElement.style.left = `calc(50% + ${horizontalAdjustment}px)`;
|
|
175
281
|
}
|
|
176
282
|
}
|
|
177
283
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
284
|
+
// Adjust vertical position for left/right tooltips that overflow edges
|
|
285
|
+
if (finalSelectedPosition === 'left' || finalSelectedPosition === 'right') {
|
|
286
|
+
let verticalAdjustment = 0;
|
|
287
|
+
|
|
288
|
+
// Check if tooltip overflows top edge
|
|
289
|
+
if (tooltipBoundsAfterPositioning.top < edgePadding) {
|
|
290
|
+
verticalAdjustment = edgePadding - tooltipBoundsAfterPositioning.top;
|
|
291
|
+
}
|
|
292
|
+
// Check if tooltip overflows bottom edge
|
|
293
|
+
else if (tooltipBoundsAfterPositioning.bottom > viewportHeight - edgePadding) {
|
|
294
|
+
verticalAdjustment = (viewportHeight - edgePadding) - tooltipBoundsAfterPositioning.bottom;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (verticalAdjustment !== 0) {
|
|
298
|
+
// Shift tooltip while preserving centered alignment
|
|
299
|
+
tooltipElement.style.top = `calc(50% + ${verticalAdjustment}px)`;
|
|
185
300
|
}
|
|
186
301
|
}
|
|
187
|
-
}
|
|
302
|
+
});
|
|
188
303
|
}
|
|
189
304
|
}
|