mirror of https://github.com/mastodon/mastodon.git
Change hover cards to not appear until the mouse stops in web UI
parent
3939352e92
commit
20852257d1
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -25,17 +25,19 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
export const FollowButton: React.FC<{
|
||||
accountId: string;
|
||||
accountId?: string;
|
||||
}> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const relationship = useAppSelector((state) =>
|
||||
state.relationships.get(accountId),
|
||||
accountId ? state.relationships.get(accountId) : undefined,
|
||||
);
|
||||
const following = relationship?.following || relationship?.requested;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
if (accountId) {
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
}
|
||||
}, [dispatch, accountId]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
|||
|
||||
export const HoverCardAccount = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ accountId: string }
|
||||
{ accountId?: string }
|
||||
>(({ accountId }, ref) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
|||
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
||||
|
||||
const offset = [-12, 4] as OffsetValue;
|
||||
const enterDelay = 650;
|
||||
const enterDelay = 500;
|
||||
const leaveDelay = 250;
|
||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||
|
||||
|
@ -23,50 +23,14 @@ 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 anchorRef = useRef<HTMLElement | null>(null);
|
||||
const scrollingRef = useRef<boolean>(false);
|
||||
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 +43,98 @@ export const HoverCardController: React.FC = () => {
|
|||
}, [handleClose, location]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
const { target } = e;
|
||||
|
||||
if (scrollingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
|
||||
cancelLeaveTimeout();
|
||||
|
||||
setEnterTimeout(() => {
|
||||
anchorRef.current = target;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: MouseEvent) => {
|
||||
if (
|
||||
e.target === anchorRef.current ||
|
||||
e.target === cardRef.current?.parentNode
|
||||
) {
|
||||
cancelEnterTimeout();
|
||||
|
||||
setLeaveTimeout(() => {
|
||||
anchorRef.current?.removeAttribute('aria-describedby');
|
||||
setOpen(false);
|
||||
setAnchor(null);
|
||||
}, leaveDelay);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollEnd = () => {
|
||||
scrollingRef.current = false;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
scrollingRef.current = 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
|
||||
|
|
Loading…
Reference in New Issue