Calculate HoverPreview position inside useLayoutEffect (#5636)

This commit is contained in:
Apoorv Mishra
2023-08-06 21:30:05 +05:30
committed by GitHub
parent 6c4e2a9d11
commit 0ddbd9c608
5 changed files with 97 additions and 74 deletions

View File

@@ -4,11 +4,7 @@ import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Text from "~/components/Text";
export const CARD_PADDING = 16;
export const CARD_WIDTH = 375;
export const THUMBNAIL_HEIGHT = 200;
export const CARD_MARGIN = 16;
const NUMBER_OF_LINES = 10;
@@ -28,6 +24,8 @@ export const Preview = styled(Link)`
0 0 1px 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
position: absolute;
min-width: 350px;
max-width: 375px;
`;
export const Title = styled.h2`
@@ -39,7 +37,9 @@ export const Title = styled.h2`
export const Info = styled(StyledText).attrs(() => ({
type: "tertiary",
size: "xsmall",
}))``;
}))`
white-space: nowrap;
`;
export const Description = styled(StyledText)`
${sharedVars}
@@ -48,6 +48,12 @@ export const Description = styled(StyledText)`
max-height: calc(var(--line-height) * ${NUMBER_OF_LINES});
`;
export const Thumbnail = styled.img`
object-fit: cover;
height: 200px;
background: ${s("menuBackground")};
`;
export const CardContent = styled.div`
overflow: hidden;
user-select: none;
@@ -57,8 +63,7 @@ export const CardContent = styled.div`
export const Card = styled.div<{ fadeOut?: boolean; $borderRadius?: string }>`
backdrop-filter: blur(10px);
background: ${s("menuBackground")};
padding: ${CARD_PADDING}px;
width: ${CARD_WIDTH}px;
padding: 16px;
font-size: 0.9em;
position: relative;

View File

@@ -12,7 +12,7 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { client } from "~/utils/ApiClient";
import { CARD_WIDTH, CARD_PADDING } from "./Components";
import { CARD_MARGIN } from "./Components";
import HoverPreviewDocument from "./HoverPreviewDocument";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
@@ -34,6 +34,35 @@ function HoverPreviewInternal({ element, onClose }: Props) {
const timerOpen = React.useRef<ReturnType<typeof setTimeout>>();
const cardRef = React.useRef<HTMLDivElement>(null);
const stores = useStores();
const [cardLeft, setCardLeft] = React.useState(0);
const [cardTop, setCardTop] = React.useState(0);
const [pointerOffset, setPointerOffset] = React.useState(0);
React.useLayoutEffect(() => {
if (isVisible && cardRef.current) {
const elem = element.getBoundingClientRect();
const card = cardRef.current.getBoundingClientRect();
const top = elem.bottom + window.scrollY;
setCardTop(top);
let left = elem.left;
let pointerOffset = elem.width / 2;
if (left + card.width > window.innerWidth) {
// shift card leftwards by the amount it went out of screen
let shiftBy = left + card.width - window.innerWidth;
// shift a littler further to leave some margin between card and window boundary
shiftBy += CARD_MARGIN;
left -= shiftBy;
// shift pointer rightwards by same amount so as to position it back correctly
pointerOffset += shiftBy;
}
setCardLeft(left);
setPointerOffset(pointerOffset);
}
}, [isVisible, element]);
const { data, request, loading } = useRequest(
React.useCallback(
@@ -122,13 +151,6 @@ function HoverPreviewInternal({ element, onClose }: Props) {
};
}, [element, startCloseTimer, data]);
const elemBounds = element.getBoundingClientRect();
const cardBounds = cardRef.current?.getBoundingClientRect();
const left = cardBounds
? Math.min(elemBounds.left, window.innerWidth - CARD_PADDING - CARD_WIDTH)
: elemBounds.left;
const leftOffset = elemBounds.left - left;
if (loading) {
return <LoadingIndicator />;
}
@@ -139,44 +161,41 @@ function HoverPreviewInternal({ element, onClose }: Props) {
return (
<Portal>
<Position
top={elemBounds.bottom + window.scrollY}
left={left}
aria-hidden
>
<div ref={cardRef}>
{isVisible ? (
<Animate
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
animate={{ opacity: 1, y: 0, pointerEvents: "auto" }}
>
{data.type === UnfurlType.Mention ? (
<HoverPreviewMention
url={data.thumbnailUrl}
title={data.title}
info={data.meta.info}
color={data.meta.color}
/>
) : data.type === UnfurlType.Document ? (
<HoverPreviewDocument
id={data.meta.id}
url={data.url}
title={data.title}
description={data.description}
info={data.meta.info}
/>
) : (
<HoverPreviewLink
url={data.url}
thumbnailUrl={data.thumbnailUrl}
title={data.title}
description={data.description}
/>
)}
<Pointer offset={leftOffset + elemBounds.width / 2} />
</Animate>
) : null}
</div>
<Position top={cardTop} left={cardLeft} aria-hidden>
{isVisible ? (
<Animate
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
animate={{ opacity: 1, y: 0, pointerEvents: "auto" }}
>
{data.type === UnfurlType.Mention ? (
<HoverPreviewMention
ref={cardRef}
url={data.thumbnailUrl}
title={data.title}
info={data.meta.info}
color={data.meta.color}
/>
) : data.type === UnfurlType.Document ? (
<HoverPreviewDocument
ref={cardRef}
id={data.meta.id}
url={data.url}
title={data.title}
description={data.description}
info={data.meta.info}
/>
) : (
<HoverPreviewLink
ref={cardRef}
url={data.url}
thumbnailUrl={data.thumbnailUrl}
title={data.title}
description={data.description}
/>
)}
<Pointer offset={pointerOffset} />
</Animate>
) : null}
</Position>
</Portal>
);

View File

@@ -23,10 +23,13 @@ type Props = {
description: string;
};
function HoverPreviewDocument({ id, url, title, info, description }: Props) {
const HoverPreviewDocument = React.forwardRef(function _HoverPreviewDocument(
{ id, url, title, info, description }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
<Preview to={url}>
<Card>
<Card ref={ref}>
<CardContent>
<Flex column gap={2}>
<Title>{title}</Title>
@@ -46,6 +49,6 @@ function HoverPreviewDocument({ id, url, title, info, description }: Props) {
</Card>
</Preview>
);
}
});
export default HoverPreviewDocument;

View File

@@ -1,6 +1,4 @@
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "~/components/Flex";
import {
Preview,
@@ -8,8 +6,7 @@ import {
Description,
Card,
CardContent,
CARD_WIDTH,
THUMBNAIL_HEIGHT,
Thumbnail,
} from "./Components";
type Props = {
@@ -23,12 +20,15 @@ type Props = {
description: string;
};
function HoverPreviewLink({ url, thumbnailUrl, title, description }: Props) {
const HoverPreviewLink = React.forwardRef(function _HoverPreviewLink(
{ url, thumbnailUrl, title, description }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column>
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
<Card>
<Card ref={ref}>
<CardContent>
<Flex column>
<Title>{title}</Title>
@@ -39,13 +39,6 @@ function HoverPreviewLink({ url, thumbnailUrl, title, description }: Props) {
</Flex>
</Preview>
);
}
const Thumbnail = styled.img`
object-fit: cover;
max-width: ${CARD_WIDTH}px;
height: ${THUMBNAIL_HEIGHT}px;
background: ${s("menuBackground")};
`;
});
export default HoverPreviewLink;

View File

@@ -15,10 +15,13 @@ type Props = {
color: string;
};
function HoverPreviewMention({ url, title, info, color }: Props) {
const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
{ url, title, info, color }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
<Preview as="div">
<Card fadeOut={false}>
<Card fadeOut={false} ref={ref}>
<CardContent>
<Flex gap={12}>
<Avatar
@@ -38,6 +41,6 @@ function HoverPreviewMention({ url, title, info, color }: Props) {
</Card>
</Preview>
);
}
});
export default HoverPreviewMention;