From 5e705f3dc7caa88a51d558d939b285318234bd5a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 22 Jul 2023 12:47:01 -0400 Subject: [PATCH] fix: Tweaks to hover card behavior --- app/components/HoverPreview/HoverPreview.tsx | 48 +++++++++++++------- app/hooks/useRequest.ts | 17 +++++-- shared/editor/components/Styles.ts | 1 + shared/editor/nodes/Mention.ts | 2 +- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/components/HoverPreview/HoverPreview.tsx b/app/components/HoverPreview/HoverPreview.tsx index f4f129f7d..93af0ca79 100644 --- a/app/components/HoverPreview/HoverPreview.tsx +++ b/app/components/HoverPreview/HoverPreview.tsx @@ -5,7 +5,9 @@ import styled from "styled-components"; import { depths, s } from "@shared/styles"; import { UnfurlType } from "@shared/types"; import LoadingIndicator from "~/components/LoadingIndicator"; +import useKeyDown from "~/hooks/useKeyDown"; import useMobile from "~/hooks/useMobile"; +import useOnClickOutside from "~/hooks/useOnClickOutside"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; import { fadeAndSlideDown } from "~/styles/animations"; @@ -14,7 +16,7 @@ import HoverPreviewDocument from "./HoverPreviewDocument"; import HoverPreviewMention from "./HoverPreviewMention"; const DELAY_OPEN = 300; -const DELAY_CLOSE = 300; +const DELAY_CLOSE = 600; const CARD_PADDING = 16; const CARD_MAX_WIDTH = 375; @@ -32,6 +34,7 @@ function HoverPreviewInternal({ element, onClose }: Props) { const timerOpen = React.useRef>(); const cardRef = React.useRef(null); const stores = useStores(); + const { data, request, loading } = useRequest( React.useCallback( () => @@ -45,40 +48,51 @@ function HoverPreviewInternal({ element, onClose }: Props) { React.useEffect(() => { if (url) { + stopOpenTimer(); void request(); } }, [url, request]); - const startCloseTimer = React.useCallback(() => { - stopOpenTimer(); - timerClose.current = setTimeout(() => { - if (isVisible) { - setVisible(false); - } + const stopOpenTimer = () => { + if (timerOpen.current) { + clearTimeout(timerOpen.current); + timerOpen.current = undefined; + } + }; + + const closePreview = React.useCallback(() => { + if (isVisible) { + stopOpenTimer(); + setVisible(false); onClose(); - }, DELAY_CLOSE); + } }, [isVisible, onClose]); + useOnClickOutside(cardRef, closePreview); + useKeyDown("Escape", closePreview); + const stopCloseTimer = () => { if (timerClose.current) { clearTimeout(timerClose.current); + timerClose.current = undefined; } }; const startOpenTimer = () => { - timerOpen.current = setTimeout(() => setVisible(true), DELAY_OPEN); - }; - - const stopOpenTimer = () => { - if (timerOpen.current) { - clearTimeout(timerOpen.current); + if (!timerOpen.current) { + timerOpen.current = setTimeout(() => setVisible(true), DELAY_OPEN); } }; + const startCloseTimer = React.useCallback(() => { + stopOpenTimer(); + timerClose.current = setTimeout(closePreview, DELAY_CLOSE); + }, [closePreview]); + React.useEffect(() => { const card = cardRef.current; - if (data && !loading) { + if (data) { startOpenTimer(); if (card) { @@ -103,7 +117,7 @@ function HoverPreviewInternal({ element, onClose }: Props) { stopCloseTimer(); }; - }, [element, startCloseTimer, data, loading]); + }, [element, startCloseTimer, data]); const elemBounds = element.getBoundingClientRect(); const cardBounds = cardRef.current?.getBoundingClientRect(); @@ -275,7 +289,7 @@ const Pointer = styled.div<{ offset: number }>` &:after { border: 7px solid transparent; - border-bottom-color: ${s("background")}; + border-bottom-color: ${s("menuBackground")}; } `; diff --git a/app/hooks/useRequest.ts b/app/hooks/useRequest.ts index c68b43d7b..e8af4b1cd 100644 --- a/app/hooks/useRequest.ts +++ b/app/hooks/useRequest.ts @@ -1,4 +1,5 @@ import * as React from "react"; +import useIsMounted from "./useIsMounted"; type RequestResponse = { /** The return value of the request function. */ @@ -20,6 +21,7 @@ type RequestResponse = { export default function useRequest( requestFn: () => Promise ): RequestResponse { + const isMounted = useIsMounted(); const [data, setData] = React.useState(); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(); @@ -28,16 +30,23 @@ export default function useRequest( setLoading(true); try { const response = await requestFn(); - setData(response); + + if (isMounted()) { + setData(response); + } return response; } catch (err) { - setError(err); + if (isMounted()) { + setError(err); + } } finally { - setLoading(false); + if (isMounted()) { + setLoading(false); + } } return undefined; - }, [requestFn]); + }, [requestFn, isMounted]); return { data, loading, error, request }; } diff --git a/shared/editor/components/Styles.ts b/shared/editor/components/Styles.ts index ee79ee5f9..4518168a4 100644 --- a/shared/editor/components/Styles.ts +++ b/shared/editor/components/Styles.ts @@ -245,6 +245,7 @@ width: 100%; padding-right: 4px; font-weight: 500; font-size: 0.9em; + cursor: default; } > div { diff --git a/shared/editor/nodes/Mention.ts b/shared/editor/nodes/Mention.ts index fe7e01a59..948c7783d 100644 --- a/shared/editor/nodes/Mention.ts +++ b/shared/editor/nodes/Mention.ts @@ -91,7 +91,7 @@ export default class Mention extends Suggestion { if ( target instanceof HTMLSpanElement && this.editor.elementRef.current?.contains(target) && - !target.className.includes("ProseMirror-widget") && + target.className.includes("mention") && (!view.editable || (view.editable && !view.hasFocus())) ) { if (this.options.onHoverLink) {