[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<{
 | 
					export const FollowButton: React.FC<{
 | 
				
			||||||
  accountId: string;
 | 
					  accountId?: string;
 | 
				
			||||||
}> = ({ accountId }) => {
 | 
					}> = ({ accountId }) => {
 | 
				
			||||||
  const intl = useIntl();
 | 
					  const intl = useIntl();
 | 
				
			||||||
  const dispatch = useAppDispatch();
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ export const FollowButton: React.FC<{
 | 
				
			||||||
    accountId ? state.accounts.get(accountId) : undefined,
 | 
					    accountId ? state.accounts.get(accountId) : undefined,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const relationship = useAppSelector((state) =>
 | 
					  const relationship = useAppSelector((state) =>
 | 
				
			||||||
    state.relationships.get(accountId),
 | 
					    accountId ? state.relationships.get(accountId) : undefined,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const following = relationship?.following || relationship?.requested;
 | 
					  const following = relationship?.following || relationship?.requested;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const HoverCardAccount = forwardRef<
 | 
					export const HoverCardAccount = forwardRef<
 | 
				
			||||||
  HTMLDivElement,
 | 
					  HTMLDivElement,
 | 
				
			||||||
  { accountId: string }
 | 
					  { accountId?: string }
 | 
				
			||||||
>(({ accountId }, ref) => {
 | 
					>(({ accountId }, ref) => {
 | 
				
			||||||
  const dispatch = useAppDispatch();
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,8 @@ import { HoverCardAccount } from 'flavours/glitch/components/hover_card_account'
 | 
				
			||||||
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
 | 
					import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const offset = [-12, 4] as OffsetValue;
 | 
					const offset = [-12, 4] as OffsetValue;
 | 
				
			||||||
const enterDelay = 650;
 | 
					const enterDelay = 750;
 | 
				
			||||||
const leaveDelay = 250;
 | 
					const leaveDelay = 150;
 | 
				
			||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
 | 
					const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isHoverCardAnchor = (element: HTMLElement) =>
 | 
					const isHoverCardAnchor = (element: HTMLElement) =>
 | 
				
			||||||
| 
						 | 
					@ -23,50 +23,12 @@ export const HoverCardController: React.FC = () => {
 | 
				
			||||||
  const [open, setOpen] = useState(false);
 | 
					  const [open, setOpen] = useState(false);
 | 
				
			||||||
  const [accountId, setAccountId] = useState<string | undefined>();
 | 
					  const [accountId, setAccountId] = useState<string | undefined>();
 | 
				
			||||||
  const [anchor, setAnchor] = useState<HTMLElement | null>(null);
 | 
					  const [anchor, setAnchor] = useState<HTMLElement | null>(null);
 | 
				
			||||||
  const cardRef = useRef<HTMLDivElement>(null);
 | 
					  const cardRef = useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
  const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
 | 
					  const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
 | 
				
			||||||
  const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
 | 
					  const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
 | 
				
			||||||
 | 
					  const [setScrollTimeout] = useTimeout();
 | 
				
			||||||
  const location = useLocation();
 | 
					  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(() => {
 | 
					  const handleClose = useCallback(() => {
 | 
				
			||||||
    cancelEnterTimeout();
 | 
					    cancelEnterTimeout();
 | 
				
			||||||
    cancelLeaveTimeout();
 | 
					    cancelLeaveTimeout();
 | 
				
			||||||
| 
						 | 
					@ -79,22 +41,119 @@ export const HoverCardController: React.FC = () => {
 | 
				
			||||||
  }, [handleClose, location]);
 | 
					  }, [handleClose, location]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  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,
 | 
					      passive: true,
 | 
				
			||||||
      capture: 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,
 | 
					      passive: true,
 | 
				
			||||||
      capture: true,
 | 
					      capture: true,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
 | 
					      document.body.removeEventListener('mouseenter', handleMouseEnter);
 | 
				
			||||||
      document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
 | 
					      document.body.removeEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					      document.body.removeEventListener('mouseleave', handleMouseLeave);
 | 
				
			||||||
 | 
					      document.removeEventListener('scroll', handleScroll);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
 | 
					  }, [
 | 
				
			||||||
 | 
					    setEnterTimeout,
 | 
				
			||||||
  if (!accountId) return null;
 | 
					    setLeaveTimeout,
 | 
				
			||||||
 | 
					    setScrollTimeout,
 | 
				
			||||||
 | 
					    cancelEnterTimeout,
 | 
				
			||||||
 | 
					    cancelLeaveTimeout,
 | 
				
			||||||
 | 
					    delayEnterTimeout,
 | 
				
			||||||
 | 
					    setOpen,
 | 
				
			||||||
 | 
					    setAccountId,
 | 
				
			||||||
 | 
					    setAnchor,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Overlay
 | 
					    <Overlay
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -185,7 +185,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
 | 
				
			||||||
  menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
 | 
					  menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const names = accounts.map(a => (
 | 
					  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>
 | 
					      <bdi>
 | 
				
			||||||
        <strong
 | 
					        <strong
 | 
				
			||||||
          className='display-name__html'
 | 
					          className='display-name__html'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,19 +2,34 @@ import { useRef, useCallback, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useTimeout = () => {
 | 
					export const useTimeout = () => {
 | 
				
			||||||
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
 | 
					  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
 | 
				
			||||||
 | 
					  const callbackRef = useRef<() => void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const set = useCallback((callback: () => void, delay: number) => {
 | 
					  const set = useCallback((callback: () => void, delay: number) => {
 | 
				
			||||||
    if (timeoutRef.current) {
 | 
					    if (timeoutRef.current) {
 | 
				
			||||||
      clearTimeout(timeoutRef.current);
 | 
					      clearTimeout(timeoutRef.current);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    callbackRef.current = callback;
 | 
				
			||||||
    timeoutRef.current = setTimeout(callback, delay);
 | 
					    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(() => {
 | 
					  const cancel = useCallback(() => {
 | 
				
			||||||
    if (timeoutRef.current) {
 | 
					    if (timeoutRef.current) {
 | 
				
			||||||
      clearTimeout(timeoutRef.current);
 | 
					      clearTimeout(timeoutRef.current);
 | 
				
			||||||
      timeoutRef.current = undefined;
 | 
					      timeoutRef.current = undefined;
 | 
				
			||||||
 | 
					      callbackRef.current = undefined;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,5 +40,5 @@ export const useTimeout = () => {
 | 
				
			||||||
    [cancel],
 | 
					    [cancel],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return [set, cancel] as const;
 | 
					  return [set, cancel, delay] as const;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11022,12 +11022,14 @@ noscript {
 | 
				
			||||||
        overflow: hidden;
 | 
					        overflow: hidden;
 | 
				
			||||||
        white-space: nowrap;
 | 
					        white-space: nowrap;
 | 
				
			||||||
        text-overflow: ellipsis;
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
 | 
					        text-align: end;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      &.verified {
 | 
					      &.verified {
 | 
				
			||||||
        dd {
 | 
					        dd {
 | 
				
			||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          align-items: center;
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          justify-content: flex-end;
 | 
				
			||||||
          gap: 4px;
 | 
					          gap: 4px;
 | 
				
			||||||
          overflow: hidden;
 | 
					          overflow: hidden;
 | 
				
			||||||
          white-space: nowrap;
 | 
					          white-space: nowrap;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue