Calculate HoverPreview position inside useLayoutEffect (#5636)
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user