@aiaiai-pt/design-system 0.51.0 → 0.51.1
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.
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* @typedef {'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'} Placement
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { calculatePopoverPosition } from './popover-position.js';
|
|
29
|
+
|
|
28
30
|
let {
|
|
29
31
|
/** @type {boolean} */
|
|
30
32
|
open = $bindable(false),
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
|
|
53
55
|
let posX = $state(0);
|
|
54
56
|
let posY = $state(0);
|
|
57
|
+
let posMaxHeight = /** @type {number | undefined} */ ($state(undefined));
|
|
55
58
|
let width = $state(0);
|
|
56
59
|
|
|
57
60
|
function reposition() {
|
|
@@ -66,27 +69,19 @@
|
|
|
66
69
|
width = rect.width;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else if (isTop && y < 0) {
|
|
79
|
-
y = rect.bottom + offset;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Clamp horizontal
|
|
83
|
-
if (x + popRect.width > viewportW) {
|
|
84
|
-
x = viewportW - popRect.width - offset;
|
|
85
|
-
}
|
|
86
|
-
if (x < 0) x = offset;
|
|
72
|
+
const { x, y, maxHeight } = calculatePopoverPosition(
|
|
73
|
+
rect,
|
|
74
|
+
{ width: popRect.width, height: popRect.height },
|
|
75
|
+
viewportH,
|
|
76
|
+
viewportW,
|
|
77
|
+
placement,
|
|
78
|
+
offset,
|
|
79
|
+
matchWidth,
|
|
80
|
+
);
|
|
87
81
|
|
|
88
82
|
posX = x;
|
|
89
83
|
posY = y;
|
|
84
|
+
posMaxHeight = maxHeight;
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
// Position + close listeners
|
|
@@ -157,6 +152,8 @@
|
|
|
157
152
|
style:left="{posX}px"
|
|
158
153
|
style:top="{posY}px"
|
|
159
154
|
style:width={matchWidth ? `${width}px` : undefined}
|
|
155
|
+
style:max-height={posMaxHeight != null ? `${posMaxHeight}px` : undefined}
|
|
156
|
+
style:overflow-y={posMaxHeight != null ? 'auto' : undefined}
|
|
160
157
|
{...rest}
|
|
161
158
|
>
|
|
162
159
|
{#if children}{@render children()}{/if}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* calculatePopoverPosition — pure positioning algorithm for Popover.svelte.
|
|
3
|
+
*
|
|
4
|
+
* Returns the fixed-position coordinate (x, y) for the popover, plus an
|
|
5
|
+
* optional maxHeight cap (in px) when neither the preferred nor the
|
|
6
|
+
* opposite side fits and we must scroll the popover content in-place.
|
|
7
|
+
*
|
|
8
|
+
* Preference order:
|
|
9
|
+
* 1. Preferred side (from `placement`) if it fits.
|
|
10
|
+
* 2. Opposite side if that fits.
|
|
11
|
+
* 3. Side with more room — capped to that room, overflow-y scrollable.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ top: number, bottom: number, left: number, right: number, width: number }} anchorRect
|
|
14
|
+
* @param {{ width: number, height: number }} popRect
|
|
15
|
+
* @param {number} viewportH
|
|
16
|
+
* @param {number} viewportW
|
|
17
|
+
* @param {string} placement — 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
|
18
|
+
* @param {number} offset — gap between anchor and popover (px)
|
|
19
|
+
* @param {boolean} [matchWidth]
|
|
20
|
+
* @returns {{ x: number, y: number, maxHeight: number | undefined }}
|
|
21
|
+
*/
|
|
22
|
+
export function calculatePopoverPosition(anchorRect: {
|
|
23
|
+
top: number;
|
|
24
|
+
bottom: number;
|
|
25
|
+
left: number;
|
|
26
|
+
right: number;
|
|
27
|
+
width: number;
|
|
28
|
+
}, popRect: {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}, viewportH: number, viewportW: number, placement: string, offset: number, matchWidth?: boolean): {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
maxHeight: number | undefined;
|
|
35
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* calculatePopoverPosition — pure positioning algorithm for Popover.svelte.
|
|
3
|
+
*
|
|
4
|
+
* Returns the fixed-position coordinate (x, y) for the popover, plus an
|
|
5
|
+
* optional maxHeight cap (in px) when neither the preferred nor the
|
|
6
|
+
* opposite side fits and we must scroll the popover content in-place.
|
|
7
|
+
*
|
|
8
|
+
* Preference order:
|
|
9
|
+
* 1. Preferred side (from `placement`) if it fits.
|
|
10
|
+
* 2. Opposite side if that fits.
|
|
11
|
+
* 3. Side with more room — capped to that room, overflow-y scrollable.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ top: number, bottom: number, left: number, right: number, width: number }} anchorRect
|
|
14
|
+
* @param {{ width: number, height: number }} popRect
|
|
15
|
+
* @param {number} viewportH
|
|
16
|
+
* @param {number} viewportW
|
|
17
|
+
* @param {string} placement — 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
|
18
|
+
* @param {number} offset — gap between anchor and popover (px)
|
|
19
|
+
* @param {boolean} [matchWidth]
|
|
20
|
+
* @returns {{ x: number, y: number, maxHeight: number | undefined }}
|
|
21
|
+
*/
|
|
22
|
+
export function calculatePopoverPosition(
|
|
23
|
+
anchorRect,
|
|
24
|
+
popRect,
|
|
25
|
+
viewportH,
|
|
26
|
+
viewportW,
|
|
27
|
+
placement,
|
|
28
|
+
offset,
|
|
29
|
+
matchWidth = false,
|
|
30
|
+
) {
|
|
31
|
+
const isTop = placement.startsWith("top");
|
|
32
|
+
const isEnd = placement.endsWith("end");
|
|
33
|
+
|
|
34
|
+
// Horizontal: preserve matchWidth behaviour for end-aligned placement
|
|
35
|
+
let x = isEnd
|
|
36
|
+
? anchorRect.right - (matchWidth ? anchorRect.width : popRect.width)
|
|
37
|
+
: anchorRect.left;
|
|
38
|
+
|
|
39
|
+
// Available room on each side (gap to viewport edge after the offset)
|
|
40
|
+
const roomAbove = anchorRect.top - offset;
|
|
41
|
+
const roomBelow = viewportH - anchorRect.bottom - offset;
|
|
42
|
+
|
|
43
|
+
const fitsAbove = popRect.height <= roomAbove;
|
|
44
|
+
const fitsBelow = popRect.height <= roomBelow;
|
|
45
|
+
|
|
46
|
+
let y;
|
|
47
|
+
let maxHeight = undefined;
|
|
48
|
+
|
|
49
|
+
if (isTop) {
|
|
50
|
+
if (fitsAbove) {
|
|
51
|
+
// Preferred side fits
|
|
52
|
+
y = anchorRect.top - popRect.height - offset;
|
|
53
|
+
} else if (fitsBelow) {
|
|
54
|
+
// Flip to below
|
|
55
|
+
y = anchorRect.bottom + offset;
|
|
56
|
+
} else {
|
|
57
|
+
// Neither fits: open on the side with more room and cap
|
|
58
|
+
if (roomAbove >= roomBelow) {
|
|
59
|
+
maxHeight = roomAbove;
|
|
60
|
+
y = anchorRect.top - maxHeight - offset; // = anchorRect.top - (anchorRect.top - offset) - offset = 0
|
|
61
|
+
} else {
|
|
62
|
+
y = anchorRect.bottom + offset;
|
|
63
|
+
maxHeight = roomBelow;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Bottom placement
|
|
68
|
+
if (fitsBelow) {
|
|
69
|
+
// Preferred side fits
|
|
70
|
+
y = anchorRect.bottom + offset;
|
|
71
|
+
} else if (fitsAbove) {
|
|
72
|
+
// Flip to above
|
|
73
|
+
y = anchorRect.top - popRect.height - offset;
|
|
74
|
+
} else {
|
|
75
|
+
// Neither fits: open on the side with more room and cap
|
|
76
|
+
if (roomBelow >= roomAbove) {
|
|
77
|
+
y = anchorRect.bottom + offset;
|
|
78
|
+
maxHeight = roomBelow;
|
|
79
|
+
} else {
|
|
80
|
+
maxHeight = roomAbove;
|
|
81
|
+
y = anchorRect.top - maxHeight - offset; // = 0
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Clamp horizontal
|
|
87
|
+
if (x + popRect.width > viewportW) {
|
|
88
|
+
x = viewportW - popRect.width - offset;
|
|
89
|
+
}
|
|
90
|
+
if (x < 0) x = offset;
|
|
91
|
+
|
|
92
|
+
return { x, y, maxHeight };
|
|
93
|
+
}
|