Skip to content

Commit b88eafa

Browse files
committed
fix: more predictable positioning for info and options box
1 parent 581e358 commit b88eafa

File tree

1 file changed

+135
-62
lines changed

1 file changed

+135
-62
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 135 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,21 +1456,64 @@ function RemoteFunctions(config = {}) {
14561456
_getBoxPosition: function(boxWidth, boxHeight) {
14571457
const elemBounds = this.element.getBoundingClientRect();
14581458
const offset = _screenOffset(this.element);
1459+
const MARGIN = 6;
14591460

1460-
let topPos = offset.top - boxHeight - 6; // 6 for just some little space to breathe
1461-
let leftPos = offset.left + elemBounds.width - boxWidth;
1461+
const viewportLeft = window.scrollX;
1462+
const viewportRight = window.scrollX + window.innerWidth;
1463+
const viewportTop = window.scrollY;
1464+
const viewportBottom = window.scrollY + window.innerHeight;
14621465

1463-
// Check if the box would go off the top of the viewport
1464-
if (elemBounds.top - boxHeight < 6) {
1465-
topPos = offset.top + elemBounds.height + 6;
1466+
// Default: Top of element, left-aligned
1467+
let topPos = offset.top - boxHeight - MARGIN;
1468+
let leftPos = offset.left;
1469+
1470+
// Check if element is very tall (covers full viewport height)
1471+
const isVeryTall = elemBounds.height >= window.innerHeight - 50;
1472+
1473+
if (isVeryTall) {
1474+
// Place inside element at top
1475+
topPos = offset.top + MARGIN;
1476+
leftPos = offset.left + MARGIN;
1477+
return {topPos, leftPos};
14661478
}
14671479

1468-
// Check if the box would go off the left of the viewport
1469-
if (leftPos < 0) {
1470-
leftPos = offset.left;
1480+
// Edge Case: Goes off top
1481+
if (topPos < viewportTop + MARGIN) {
1482+
// Try right side
1483+
const rightSideTop = offset.top;
1484+
const rightSideLeft = offset.left + elemBounds.width + MARGIN;
1485+
1486+
if (rightSideLeft + boxWidth <= viewportRight - MARGIN) {
1487+
topPos = rightSideTop;
1488+
leftPos = rightSideLeft;
1489+
} else {
1490+
// Try left side
1491+
const leftSideLeft = offset.left - boxWidth - MARGIN;
1492+
1493+
if (leftSideLeft >= viewportLeft + MARGIN) {
1494+
topPos = offset.top;
1495+
leftPos = leftSideLeft;
1496+
} else {
1497+
// Last resort: Below element (below info box)
1498+
// Will be positioned below info box by checking overlap later
1499+
topPos = offset.top + elemBounds.height + MARGIN;
1500+
leftPos = offset.left;
1501+
}
1502+
}
14711503
}
14721504

1473-
return {topPos: topPos, leftPos: leftPos};
1505+
// Handle horizontal viewport boundaries
1506+
if (leftPos + boxWidth > viewportRight - MARGIN) {
1507+
// Calculate exact overflow and shift left
1508+
const overflow = (leftPos + boxWidth) - (viewportRight - MARGIN);
1509+
leftPos -= overflow;
1510+
}
1511+
1512+
if (leftPos < viewportLeft + MARGIN) {
1513+
leftPos = viewportLeft + MARGIN;
1514+
}
1515+
1516+
return {topPos, leftPos};
14741517
},
14751518

14761519
_style: function() {
@@ -1976,73 +2019,103 @@ function RemoteFunctions(config = {}) {
19762019
_getBoxPosition: function(boxDimensions, overlap = false) {
19772020
const elemBounds = this.element.getBoundingClientRect();
19782021
const offset = _screenOffset(this.element);
1979-
let topPos = 0;
1980-
let leftPos = 0;
2022+
const MARGIN = 6;
2023+
2024+
const viewportLeft = window.scrollX;
2025+
const viewportRight = window.scrollX + window.innerWidth;
2026+
const viewportTop = window.scrollY;
2027+
const viewportBottom = window.scrollY + window.innerHeight;
2028+
2029+
// Default: Bottom of element, left-aligned
2030+
let topPos = offset.top + elemBounds.height + MARGIN;
2031+
let leftPos = offset.left;
2032+
2033+
// Check if element is very tall (covers full viewport height)
2034+
const isVeryTall = elemBounds.height >= window.innerHeight - 50;
19812035

1982-
if (overlap) {
1983-
topPos = offset.top + 2;
1984-
leftPos = offset.left + elemBounds.width + 6; // positioning at the right side
2036+
if (isVeryTall) {
2037+
// Place inside element at bottom
2038+
topPos = offset.top + elemBounds.height - boxDimensions.height - MARGIN;
2039+
leftPos = offset.left + MARGIN;
2040+
return {topPos, leftPos};
2041+
}
2042+
2043+
// Edge Case: Goes off bottom
2044+
if (topPos + boxDimensions.height > viewportBottom - MARGIN) {
2045+
// Try right side
2046+
const rightSideTop = offset.top;
2047+
const rightSideLeft = offset.left + elemBounds.width + MARGIN;
19852048

1986-
// Check if overlap position would go off the right of the viewport
1987-
if (leftPos + boxDimensions.width > window.innerWidth) {
1988-
leftPos = offset.left - boxDimensions.width - 6; // positioning at the left side
2049+
if (rightSideLeft + boxDimensions.width <= viewportRight - MARGIN) {
2050+
topPos = rightSideTop;
2051+
leftPos = rightSideLeft;
2052+
} else {
2053+
// Try left side
2054+
const leftSideLeft = offset.left - boxDimensions.width - MARGIN;
19892055

1990-
if (leftPos < 0) { // if left positioning not perfect, position at bottom
1991-
topPos = offset.top + elemBounds.height + 6;
2056+
if (leftSideLeft >= viewportLeft + MARGIN) {
2057+
topPos = offset.top;
2058+
leftPos = leftSideLeft;
2059+
} else {
2060+
// Last resort: Above element (above options box)
2061+
// Will be positioned above options box by checking overlap later
2062+
topPos = offset.top - boxDimensions.height - MARGIN;
19922063
leftPos = offset.left;
19932064

1994-
// if bottom position not perfect, move at top above the more options box
1995-
if (elemBounds.bottom + 6 + boxDimensions.height > window.innerHeight) {
1996-
topPos = offset.top - boxDimensions.height - 34; // 34 is for moreOptions box height
1997-
leftPos = offset.left;
2065+
// If still goes off top, place inside element
2066+
if (topPos < viewportTop + MARGIN) {
2067+
topPos = offset.top + MARGIN;
19982068
}
19992069
}
20002070
}
2001-
} else {
2002-
topPos = offset.top - boxDimensions.height - 6; // 6 for just some little space to breathe
2003-
leftPos = offset.left;
2071+
}
20042072

2005-
if (elemBounds.top - boxDimensions.height < 6) {
2006-
// check if placing the box below would cause viewport height increase
2007-
// we need this or else it might cause a flickering issue
2008-
// read this to know why flickering occurs:
2009-
// when we hover over the bottom part of a tall element, the info box appears below it.
2010-
// this increases the live preview height, which makes the cursor position relatively
2011-
// higher due to content shift. the cursor then moves out of the element boundary,
2012-
// ending the hover state. this makes the info box disappear, decreasing the height
2013-
// back, causing the cursor to fall back into the element, restarting the hover cycle.
2014-
// this creates a continuous flickering loop.
2015-
const bottomPosition = offset.top + elemBounds.height + 6;
2016-
const wouldIncreaseViewportHeight = bottomPosition + boxDimensions.height > window.innerHeight;
2017-
2018-
// we only need to use floating position during hover mode (not on click mode)
2019-
const isHoverMode = shouldShowHighlightOnHover();
2020-
const shouldUseFloatingPosition = wouldIncreaseViewportHeight && isHoverMode;
2021-
2022-
if (shouldUseFloatingPosition) {
2023-
// float over element at bottom-right to prevent layout shift during hover
2024-
topPos = offset.top + elemBounds.height - boxDimensions.height - 6;
2025-
leftPos = offset.left + elemBounds.width - boxDimensions.width;
2026-
2027-
// make sure it doesn't go off-screen
2028-
if (leftPos < 0) {
2029-
leftPos = offset.left; // align to left edge of element
2030-
}
2031-
if (topPos < 0) {
2032-
topPos = offset.top + 6; // for the top of element
2033-
}
2034-
} else {
2035-
topPos = bottomPosition;
2073+
// Handle overlap with options box
2074+
if (overlap && _nodeMoreOptionsBox && _nodeMoreOptionsBox._shadow) {
2075+
const optionsBox = _nodeMoreOptionsBox._shadow.querySelector('.phoenix-more-options-box');
2076+
if (optionsBox) {
2077+
const optionsRect = optionsBox.getBoundingClientRect();
2078+
const optionsOffset = _screenOffset(optionsBox);
2079+
2080+
// Check if we overlap
2081+
const infoBox = {
2082+
left: leftPos,
2083+
top: topPos,
2084+
right: leftPos + boxDimensions.width,
2085+
bottom: topPos + boxDimensions.height
2086+
};
2087+
2088+
const moreOptionsBox = {
2089+
left: optionsOffset.left,
2090+
top: optionsOffset.top,
2091+
right: optionsOffset.left + optionsRect.width,
2092+
bottom: optionsOffset.top + optionsRect.height
2093+
};
2094+
2095+
const isOverlapping = !(infoBox.right < moreOptionsBox.left ||
2096+
moreOptionsBox.right < infoBox.left ||
2097+
infoBox.bottom < moreOptionsBox.top ||
2098+
moreOptionsBox.bottom < infoBox.top);
2099+
2100+
if (isOverlapping) {
2101+
// Stack vertically below options box
2102+
topPos = moreOptionsBox.bottom + MARGIN;
2103+
leftPos = offset.left;
20362104
}
20372105
}
2106+
}
20382107

2039-
// Check if the box would go off the right of the viewport
2040-
if (leftPos + boxDimensions.width > window.innerWidth) {
2041-
leftPos = window.innerWidth - boxDimensions.width - 10;
2042-
}
2108+
// Handle horizontal viewport boundaries
2109+
if (leftPos + boxDimensions.width > viewportRight - MARGIN) {
2110+
const overflow = (leftPos + boxDimensions.width) - (viewportRight - MARGIN);
2111+
leftPos -= overflow;
20432112
}
20442113

2045-
return {topPos: topPos, leftPos: leftPos};
2114+
if (leftPos < viewportLeft + MARGIN) {
2115+
leftPos = viewportLeft + MARGIN;
2116+
}
2117+
2118+
return {topPos, leftPos};
20462119
},
20472120

20482121
_style: function() {

0 commit comments

Comments
 (0)