fix: re-position hover preview correctly to prevent going out of page bounds (#5702)
This commit is contained in:
@@ -4,7 +4,7 @@ import styled, { css } from "styled-components";
|
|||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
|
|
||||||
export const CARD_MARGIN = 16;
|
export const CARD_MARGIN = 10;
|
||||||
|
|
||||||
const NUMBER_OF_LINES = 10;
|
const NUMBER_OF_LINES = 10;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { m } from "framer-motion";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Portal } from "react-portal";
|
import { Portal } from "react-portal";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { depths, s } from "@shared/styles";
|
import { depths } from "@shared/styles";
|
||||||
import { UnfurlType } from "@shared/types";
|
import { UnfurlType } from "@shared/types";
|
||||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||||
import useEventListener from "~/hooks/useEventListener";
|
import useEventListener from "~/hooks/useEventListener";
|
||||||
@@ -27,6 +27,14 @@ type Props = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
UP,
|
||||||
|
DOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
const POINTER_HEIGHT = 22;
|
||||||
|
const POINTER_WIDTH = 22;
|
||||||
|
|
||||||
function HoverPreviewInternal({ element, onClose }: Props) {
|
function HoverPreviewInternal({ element, onClose }: Props) {
|
||||||
const url = element.href || element.dataset.url;
|
const url = element.href || element.dataset.url;
|
||||||
const [isVisible, setVisible] = React.useState(false);
|
const [isVisible, setVisible] = React.useState(false);
|
||||||
@@ -36,31 +44,46 @@ function HoverPreviewInternal({ element, onClose }: Props) {
|
|||||||
const stores = useStores();
|
const stores = useStores();
|
||||||
const [cardLeft, setCardLeft] = React.useState(0);
|
const [cardLeft, setCardLeft] = React.useState(0);
|
||||||
const [cardTop, setCardTop] = React.useState(0);
|
const [cardTop, setCardTop] = React.useState(0);
|
||||||
const [pointerOffset, setPointerOffset] = React.useState(0);
|
const [pointerLeft, setPointerLeft] = React.useState(0);
|
||||||
|
const [pointerTop, setPointerTop] = React.useState(0);
|
||||||
|
const [pointerDir, setPointerDir] = React.useState(Direction.UP);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (isVisible && cardRef.current) {
|
if (isVisible && cardRef.current) {
|
||||||
const elem = element.getBoundingClientRect();
|
const elem = element.getBoundingClientRect();
|
||||||
const card = cardRef.current.getBoundingClientRect();
|
const card = cardRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
const top = elem.bottom + window.scrollY;
|
let cTop = elem.bottom + window.scrollY + CARD_MARGIN;
|
||||||
setCardTop(top);
|
let pTop = -POINTER_HEIGHT;
|
||||||
|
let pDir = Direction.UP;
|
||||||
|
if (cTop + card.height > window.innerHeight + window.scrollY) {
|
||||||
|
// shift card upwards if it goes out of screen
|
||||||
|
const bottom = elem.top + window.scrollY;
|
||||||
|
cTop = bottom - card.height;
|
||||||
|
// shift a little further to leave some margin between card and element boundary
|
||||||
|
cTop -= CARD_MARGIN;
|
||||||
|
// pointer should be shifted downwards to align with card's bottom
|
||||||
|
pTop = card.height;
|
||||||
|
pDir = Direction.DOWN;
|
||||||
|
}
|
||||||
|
setCardTop(cTop);
|
||||||
|
setPointerTop(pTop);
|
||||||
|
setPointerDir(pDir);
|
||||||
|
|
||||||
let left = elem.left;
|
let cLeft = elem.left;
|
||||||
let pointerOffset = elem.width / 2;
|
let pLeft = elem.width / 2;
|
||||||
if (left + card.width > window.innerWidth) {
|
if (cLeft + card.width > window.innerWidth) {
|
||||||
// shift card leftwards by the amount it went out of screen
|
// shift card leftwards by the amount it went out of screen
|
||||||
let shiftBy = left + card.width - window.innerWidth;
|
let shiftBy = cLeft + card.width - window.innerWidth;
|
||||||
// shift a littler further to leave some margin between card and window boundary
|
// shift a little further to leave some margin between card and window boundary
|
||||||
shiftBy += CARD_MARGIN;
|
shiftBy += CARD_MARGIN;
|
||||||
left -= shiftBy;
|
cLeft -= shiftBy;
|
||||||
|
|
||||||
// shift pointer rightwards by same amount so as to position it back correctly
|
// shift pointer rightwards by same amount so as to position it back correctly
|
||||||
pointerOffset += shiftBy;
|
pLeft += shiftBy;
|
||||||
}
|
}
|
||||||
setCardLeft(left);
|
setCardLeft(cLeft);
|
||||||
|
setPointerLeft(pLeft);
|
||||||
setPointerOffset(pointerOffset);
|
|
||||||
}
|
}
|
||||||
}, [isVisible, element]);
|
}, [isVisible, element]);
|
||||||
|
|
||||||
@@ -193,7 +216,11 @@ function HoverPreviewInternal({ element, onClose }: Props) {
|
|||||||
description={data.description}
|
description={data.description}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Pointer offset={pointerOffset} />
|
<Pointer
|
||||||
|
top={pointerTop}
|
||||||
|
left={pointerLeft}
|
||||||
|
direction={pointerDir}
|
||||||
|
/>
|
||||||
</Animate>
|
</Animate>
|
||||||
) : null}
|
) : null}
|
||||||
</Position>
|
</Position>
|
||||||
@@ -217,7 +244,6 @@ const Animate = styled(m.div)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
|
const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
|
||||||
margin-top: 10px;
|
|
||||||
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
|
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
|
||||||
z-index: ${depths.hoverPreview};
|
z-index: ${depths.hoverPreview};
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -227,11 +253,11 @@ const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
|
|||||||
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
|
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Pointer = styled.div<{ offset: number }>`
|
const Pointer = styled.div<{ top: number; left: number; direction: Direction }>`
|
||||||
top: -22px;
|
top: ${(props) => props.top}px;
|
||||||
left: ${(props) => props.offset}px;
|
left: ${(props) => props.left}px;
|
||||||
width: 22px;
|
width: ${POINTER_WIDTH}px;
|
||||||
height: 22px;
|
height: ${POINTER_HEIGHT}px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -241,20 +267,26 @@ const Pointer = styled.div<{ offset: number }>`
|
|||||||
content: "";
|
content: "";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
${({ direction }) => (direction === Direction.UP ? "bottom: 0" : "top: 0")};
|
||||||
right: 0;
|
${({ direction }) => (direction === Direction.UP ? "right: 0" : "left: 0")};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
border: 8px solid transparent;
|
border: 8px solid transparent;
|
||||||
border-bottom-color: ${(props) =>
|
${({ direction, theme }) =>
|
||||||
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
|
direction === Direction.UP
|
||||||
right: -1px;
|
? `border-bottom-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`
|
||||||
|
: `border-top-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`};
|
||||||
|
${({ direction }) =>
|
||||||
|
direction === Direction.UP ? "right: -1px" : "left: -1px"};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
border: 7px solid transparent;
|
border: 7px solid transparent;
|
||||||
border-bottom-color: ${s("menuBackground")};
|
${({ direction, theme }) =>
|
||||||
|
direction === Direction.UP
|
||||||
|
? `border-bottom-color: ${theme.menuBackground}`
|
||||||
|
: `border-top-color: ${theme.menuBackground}`};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ const HoverPreviewLink = React.forwardRef(function _HoverPreviewLink(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
|
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
|
||||||
<Flex column>
|
<Flex column ref={ref}>
|
||||||
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
|
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
|
||||||
<Card ref={ref}>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Flex column>
|
<Flex column>
|
||||||
<Title>{title}</Title>
|
<Title>{title}</Title>
|
||||||
|
|||||||
Reference in New Issue
Block a user