fix: re-position hover preview correctly to prevent going out of page bounds (#5702)

This commit is contained in:
Apoorv Mishra
2023-08-20 16:42:05 +05:30
committed by GitHub
parent 546022e5d6
commit c3a8858c6b
3 changed files with 62 additions and 30 deletions

View File

@@ -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;

View File

@@ -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}`};
} }
`; `;

View File

@@ -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>