[Glitch] Change hover cards to not appear until the mouse stops in web UI
Port b728c0e8ce to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
			
			
This commit is contained in:
		
							parent
							
								
									92dcc50278
								
							
						
					
					
						commit
						dcfd39991b
					
				| 
						 | 
				
			
			@ -26,7 +26,7 @@ const messages = defineMessages({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
export const FollowButton: React.FC<{
 | 
			
		||||
  accountId: string;
 | 
			
		||||
  accountId?: string;
 | 
			
		||||
}> = ({ accountId }) => {
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ export const FollowButton: React.FC<{
 | 
			
		|||
    accountId ? state.accounts.get(accountId) : undefined,
 | 
			
		||||
  );
 | 
			
		||||
  const relationship = useAppSelector((state) =>
 | 
			
		||||
    state.relationships.get(accountId),
 | 
			
		||||
    accountId ? state.relationships.get(accountId) : undefined,
 | 
			
		||||
  );
 | 
			
		||||
  const following = relationship?.following || relationship?.requested;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
 | 
			
		|||
 | 
			
		||||
export const HoverCardAccount = forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  { accountId: string }
 | 
			
		||||
  { accountId?: string }
 | 
			
		||||
>(({ accountId }, ref) => {
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,8 +12,8 @@ import { HoverCardAccount } from 'flavours/glitch/components/hover_card_account'
 | 
			
		|||
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
 | 
			
		||||
 | 
			
		||||
const offset = [-12, 4] as OffsetValue;
 | 
			
		||||
const enterDelay = 650;
 | 
			
		||||
const leaveDelay = 250;
 | 
			
		||||
const enterDelay = 750;
 | 
			
		||||
const leaveDelay = 150;
 | 
			
		||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
 | 
			
		||||
 | 
			
		||||
const isHoverCardAnchor = (element: HTMLElement) =>
 | 
			
		||||
| 
						 | 
				
			
			@ -23,50 +23,12 @@ export const HoverCardController: React.FC = () => {
 | 
			
		|||
  const [open, setOpen] = useState(false);
 | 
			
		||||
  const [accountId, setAccountId] = useState<string | undefined>();
 | 
			
		||||
  const [anchor, setAnchor] = useState<HTMLElement | null>(null);
 | 
			
		||||
  const cardRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const cardRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
  const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
 | 
			
		||||
  const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
 | 
			
		||||
  const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
 | 
			
		||||
  const [setScrollTimeout] = useTimeout();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
 | 
			
		||||
  const handleAnchorMouseEnter = useCallback(
 | 
			
		||||
    (e: MouseEvent) => {
 | 
			
		||||
      const { target } = e;
 | 
			
		||||
 | 
			
		||||
      if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
 | 
			
		||||
        cancelLeaveTimeout();
 | 
			
		||||
 | 
			
		||||
        setEnterTimeout(() => {
 | 
			
		||||
          target.setAttribute('aria-describedby', 'hover-card');
 | 
			
		||||
          setAnchor(target);
 | 
			
		||||
          setOpen(true);
 | 
			
		||||
          setAccountId(
 | 
			
		||||
            target.getAttribute('data-hover-card-account') ?? undefined,
 | 
			
		||||
          );
 | 
			
		||||
        }, enterDelay);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (target === cardRef.current?.parentNode) {
 | 
			
		||||
        cancelLeaveTimeout();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleAnchorMouseLeave = useCallback(
 | 
			
		||||
    (e: MouseEvent) => {
 | 
			
		||||
      if (e.target === anchor || e.target === cardRef.current?.parentNode) {
 | 
			
		||||
        cancelEnterTimeout();
 | 
			
		||||
 | 
			
		||||
        setLeaveTimeout(() => {
 | 
			
		||||
          anchor?.removeAttribute('aria-describedby');
 | 
			
		||||
          setOpen(false);
 | 
			
		||||
          setAnchor(null);
 | 
			
		||||
        }, leaveDelay);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleClose = useCallback(() => {
 | 
			
		||||
    cancelEnterTimeout();
 | 
			
		||||
    cancelLeaveTimeout();
 | 
			
		||||
| 
						 | 
				
			
			@ -79,22 +41,119 @@ export const HoverCardController: React.FC = () => {
 | 
			
		|||
  }, [handleClose, location]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
 | 
			
		||||
    let isScrolling = false;
 | 
			
		||||
    let currentAnchor: HTMLElement | null = null;
 | 
			
		||||
 | 
			
		||||
    const open = (target: HTMLElement) => {
 | 
			
		||||
      target.setAttribute('aria-describedby', 'hover-card');
 | 
			
		||||
      setOpen(true);
 | 
			
		||||
      setAnchor(target);
 | 
			
		||||
      setAccountId(target.getAttribute('data-hover-card-account') ?? undefined);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const close = () => {
 | 
			
		||||
      currentAnchor?.removeAttribute('aria-describedby');
 | 
			
		||||
      currentAnchor = null;
 | 
			
		||||
      setOpen(false);
 | 
			
		||||
      setAnchor(null);
 | 
			
		||||
      setAccountId(undefined);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleMouseEnter = (e: MouseEvent) => {
 | 
			
		||||
      const { target } = e;
 | 
			
		||||
 | 
			
		||||
      // We've exited the window
 | 
			
		||||
      if (!(target instanceof HTMLElement)) {
 | 
			
		||||
        close();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // We've entered an anchor
 | 
			
		||||
      if (!isScrolling && isHoverCardAnchor(target)) {
 | 
			
		||||
        cancelLeaveTimeout();
 | 
			
		||||
 | 
			
		||||
        currentAnchor?.removeAttribute('aria-describedby');
 | 
			
		||||
        currentAnchor = target;
 | 
			
		||||
 | 
			
		||||
        setEnterTimeout(() => {
 | 
			
		||||
          open(target);
 | 
			
		||||
        }, enterDelay);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // We've entered the hover card
 | 
			
		||||
      if (
 | 
			
		||||
        !isScrolling &&
 | 
			
		||||
        (target === currentAnchor || target === cardRef.current)
 | 
			
		||||
      ) {
 | 
			
		||||
        cancelLeaveTimeout();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleMouseLeave = (e: MouseEvent) => {
 | 
			
		||||
      if (!currentAnchor) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (e.target === currentAnchor || e.target === cardRef.current) {
 | 
			
		||||
        cancelEnterTimeout();
 | 
			
		||||
 | 
			
		||||
        setLeaveTimeout(() => {
 | 
			
		||||
          close();
 | 
			
		||||
        }, leaveDelay);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleScrollEnd = () => {
 | 
			
		||||
      isScrolling = false;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleScroll = () => {
 | 
			
		||||
      isScrolling = true;
 | 
			
		||||
      cancelEnterTimeout();
 | 
			
		||||
      setScrollTimeout(handleScrollEnd, 100);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleMouseMove = () => {
 | 
			
		||||
      delayEnterTimeout(enterDelay);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    document.body.addEventListener('mouseenter', handleMouseEnter, {
 | 
			
		||||
      passive: true,
 | 
			
		||||
      capture: true,
 | 
			
		||||
    });
 | 
			
		||||
    document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
 | 
			
		||||
 | 
			
		||||
    document.body.addEventListener('mousemove', handleMouseMove, {
 | 
			
		||||
      passive: true,
 | 
			
		||||
      capture: false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    document.body.addEventListener('mouseleave', handleMouseLeave, {
 | 
			
		||||
      passive: true,
 | 
			
		||||
      capture: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('scroll', handleScroll, {
 | 
			
		||||
      passive: true,
 | 
			
		||||
      capture: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
 | 
			
		||||
      document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
 | 
			
		||||
      document.body.removeEventListener('mouseenter', handleMouseEnter);
 | 
			
		||||
      document.body.removeEventListener('mousemove', handleMouseMove);
 | 
			
		||||
      document.body.removeEventListener('mouseleave', handleMouseLeave);
 | 
			
		||||
      document.removeEventListener('scroll', handleScroll);
 | 
			
		||||
    };
 | 
			
		||||
  }, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
 | 
			
		||||
 | 
			
		||||
  if (!accountId) return null;
 | 
			
		||||
  }, [
 | 
			
		||||
    setEnterTimeout,
 | 
			
		||||
    setLeaveTimeout,
 | 
			
		||||
    setScrollTimeout,
 | 
			
		||||
    cancelEnterTimeout,
 | 
			
		||||
    cancelLeaveTimeout,
 | 
			
		||||
    delayEnterTimeout,
 | 
			
		||||
    setOpen,
 | 
			
		||||
    setAccountId,
 | 
			
		||||
    setAnchor,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Overlay
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -185,7 +185,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
 | 
			
		|||
  menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
 | 
			
		||||
 | 
			
		||||
  const names = accounts.map(a => (
 | 
			
		||||
    <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}>
 | 
			
		||||
    <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} data-hover-card-account={a.get('id')}>
 | 
			
		||||
      <bdi>
 | 
			
		||||
        <strong
 | 
			
		||||
          className='display-name__html'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,19 +2,34 @@ import { useRef, useCallback, useEffect } from 'react';
 | 
			
		|||
 | 
			
		||||
export const useTimeout = () => {
 | 
			
		||||
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
 | 
			
		||||
  const callbackRef = useRef<() => void>();
 | 
			
		||||
 | 
			
		||||
  const set = useCallback((callback: () => void, delay: number) => {
 | 
			
		||||
    if (timeoutRef.current) {
 | 
			
		||||
      clearTimeout(timeoutRef.current);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    callbackRef.current = callback;
 | 
			
		||||
    timeoutRef.current = setTimeout(callback, delay);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const delay = useCallback((delay: number) => {
 | 
			
		||||
    if (timeoutRef.current) {
 | 
			
		||||
      clearTimeout(timeoutRef.current);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!callbackRef.current) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    timeoutRef.current = setTimeout(callbackRef.current, delay);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const cancel = useCallback(() => {
 | 
			
		||||
    if (timeoutRef.current) {
 | 
			
		||||
      clearTimeout(timeoutRef.current);
 | 
			
		||||
      timeoutRef.current = undefined;
 | 
			
		||||
      callbackRef.current = undefined;
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,5 +40,5 @@ export const useTimeout = () => {
 | 
			
		|||
    [cancel],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return [set, cancel] as const;
 | 
			
		||||
  return [set, cancel, delay] as const;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11022,12 +11022,14 @@ noscript {
 | 
			
		|||
        overflow: hidden;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        text-overflow: ellipsis;
 | 
			
		||||
        text-align: end;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.verified {
 | 
			
		||||
        dd {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          justify-content: flex-end;
 | 
			
		||||
          gap: 4px;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue