@aiaiai-pt/design-system 0.51.0 → 0.51.2
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() {
|
|
@@ -59,6 +62,12 @@
|
|
|
59
62
|
|
|
60
63
|
const rect = anchor.getBoundingClientRect();
|
|
61
64
|
const popRect = popoverEl.getBoundingClientRect();
|
|
65
|
+
// Natural height, not the rendered box: once capped, the box reads the cap,
|
|
66
|
+
// which always "fits", and the cap would drop on the next reposition (the
|
|
67
|
+
// capture scroll listener fires when the popover's own content scrolls).
|
|
68
|
+
// scrollHeight is the content height; the rest of the box is its borders.
|
|
69
|
+
const naturalHeight =
|
|
70
|
+
popoverEl.scrollHeight + (popRect.height - popoverEl.clientHeight);
|
|
62
71
|
const viewportH = window.innerHeight;
|
|
63
72
|
const viewportW = window.innerWidth;
|
|
64
73
|
|
|
@@ -66,27 +75,19 @@
|
|
|
66
75
|
width = rect.width;
|
|
67
76
|
}
|
|
68
77
|
|
|
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;
|
|
78
|
+
const { x, y, maxHeight } = calculatePopoverPosition(
|
|
79
|
+
rect,
|
|
80
|
+
{ width: popRect.width, height: naturalHeight },
|
|
81
|
+
viewportH,
|
|
82
|
+
viewportW,
|
|
83
|
+
placement,
|
|
84
|
+
offset,
|
|
85
|
+
matchWidth,
|
|
86
|
+
);
|
|
87
87
|
|
|
88
88
|
posX = x;
|
|
89
89
|
posY = y;
|
|
90
|
+
posMaxHeight = maxHeight;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
// Position + close listeners
|
|
@@ -157,6 +158,8 @@
|
|
|
157
158
|
style:left="{posX}px"
|
|
158
159
|
style:top="{posY}px"
|
|
159
160
|
style:width={matchWidth ? `${width}px` : undefined}
|
|
161
|
+
style:max-height={posMaxHeight != null ? `${posMaxHeight}px` : undefined}
|
|
162
|
+
style:overflow-y={posMaxHeight != null ? 'auto' : undefined}
|
|
160
163
|
{...rest}
|
|
161
164
|
>
|
|
162
165
|
{#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
|
+
}
|