make useAction...ClickedOutside use an array of elements to click outside

This commit is contained in:
Michael Peters 2022-02-13 10:42:03 -06:00
parent a6231610a7
commit e0d0b2a9df
4 changed files with 75 additions and 52 deletions

View File

@ -20,7 +20,7 @@ const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
if (childRootRef) {
// this is alright to do since childRootRef (the ref itself) should never change for each component using this element
// eslint-disable-next-line react-hooks/rules-of-hooks
useActionWhenEscapeOrClickedOrContextOutsideEffect(childRootRef, () => setOverlay(null));
useActionWhenEscapeOrClickedOrContextOutsideEffect([childRootRef], () => setOverlay(null));
}
const keyDownHandler = useCallback(

View File

@ -18,10 +18,16 @@ const ContextMenu: FC<ContextMenuProps> = (props: ContextMenuProps) => {
const rootRef = useRef<HTMLDivElement>(null);
useActionWhenEscapeOrClickedOrContextOutsideEffect(rootRef, close);
useActionWhenEscapeOrClickedOrContextOutsideEffect([rootRef], close);
return (
<Context rootRef={rootRef} relativeToRef={relativeToRef} relativeToPos={relativeToPos} alignment={alignment} realignDeps={realignDeps}>
<Context
rootRef={rootRef}
relativeToRef={relativeToRef}
relativeToPos={relativeToPos}
alignment={alignment}
realignDeps={realignDeps}
>
<div className="menu">{children}</div>
</Context>
);

View File

@ -7,11 +7,19 @@ import React, { FC, useCallback, useMemo, useRef } from 'react';
import CombinedGuild from '../../guild-combined';
import ElementsUtil, { IAlignment } from '../require/elements-util';
import DownloadButton from '../components/button-download';
import { useContextClickContextMenu } from '../require/react-helper';
import {
useActionWhenEscapeOrClickedOrContextOutsideEffect,
useContextClickContextMenu,
} from '../require/react-helper';
import ImageContextMenu from '../contexts/context-menu-image';
import Overlay from '../components/overlay';
import { useRecoilValue } from 'recoil';
import { guildResourceSoftImgSrcState, guildResourceState, useRecoilValueSoftImgSrc } from '../require/atoms';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
guildResourceSoftImgSrcState,
guildResourceState,
overlayState,
useRecoilValueSoftImgSrc,
} from '../require/atoms';
import { isFailed, isLoaded } from '../require/loadables';
export interface ImageOverlayProps {
@ -23,7 +31,11 @@ export interface ImageOverlayProps {
const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
const { guild, resourceId, resourceName } = props;
const setOverlay = useSetRecoilState(overlayState);
const rootRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const dltxtRef = useRef<HTMLDivElement>(null);
// NOTE: These will update together. How convenient!
const resource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId }));
@ -46,13 +58,7 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
[resource, resourceName],
);
const onBaseClick = useCallback(
(e: MouseEvent) => {
if (!rootRef.current) return 1;
return 2;
},
[rootRef],
);
useActionWhenEscapeOrClickedOrContextOutsideEffect([imgRef, dltxtRef], () => setOverlay(null));
const sizeText = useMemo(() => {
if (isFailed(resource)) return 'Failed to load';
@ -61,10 +67,10 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
}, [resource]);
return (
<Overlay childRootRef={rootRef}>
<Overlay>
<div ref={rootRef} className="content popup-image">
<img src={imgSrc} alt={resourceName} title={resourceName} onContextMenu={onContextMenu} />
<div className="download">
<img ref={imgRef} src={imgSrc} alt={resourceName} title={resourceName} onContextMenu={onContextMenu} />
<div ref={dltxtRef} className="download">
<div className="info">
<div className="name">{resourceName}</div>
<div className="size">{sizeText}</div>

View File

@ -362,75 +362,86 @@ export function useScrollableCallables<T>(
// makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside
/** calls the close action when you hit escape or click outside of the ref element */
export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
ref: RefObject<HTMLElement>,
refs: RefObject<HTMLElement>[],
actionFunc: () => void,
) {
// have to use a ref here and not states since we can't re-assign state between mouseup and click
const mouseRef = useRef<{ mouseDownTarget: Node | null; mouseUpTarget: Node | null }>({
mouseDownTarget: null,
mouseUpTarget: null,
});
const data = useRef<
Map<number, { ref: RefObject<HTMLElement>; mouseDownTarget: Node | null; mouseUpTarget: Node | null }>
>(new Map());
useEffect(() => {
data.current.clear();
for (let i = 0; i < refs.length; ++i) {
const ref = refs[i] as RefObject<HTMLElement>;
data.current.set(i, { ref, mouseDownTarget: null, mouseUpTarget: null });
}
}, [refs]);
const handleClickOutside = useCallback(
(event: MouseEvent) => {
//console.log('current:', ref.current, 'target:', event.target, 'mouseDownTarget:', mouseRef.current.mouseDownTarget, 'mouseUpTarget:', mouseRef.current.mouseUpTarget);
for (const [_idx, { ref, mouseDownTarget, mouseUpTarget }] of data.current) {
if (!ref.current) return;
if (!ref.current) return;
// event is inside this ref's element, do not run the action func
// casting here is OK. https://stackoverflow.com/q/61164018
if (ref.current.contains(event.target as Node)) return;
// casting here is OK. https://stackoverflow.com/q/61164018
if (ref.current.contains(event.target as Node)) return;
if (mouseRef.current.mouseDownTarget !== null || mouseRef.current.mouseUpTarget !== null) return;
if (mouseDownTarget !== null || mouseUpTarget !== null) return;
}
actionFunc();
},
[ref, mouseRef, actionFunc],
[actionFunc],
);
const handleMouseDown = useCallback(
(event: MouseEvent) => {
if (!ref.current) return;
//console.log('mouse down. Contains: ' + ref.current.contains(event.target as Node));
if (ref.current.contains(event.target as Node)) {
mouseRef.current.mouseDownTarget = event.target as Node;
const handleMouseDown = useCallback((event: MouseEvent) => {
//console.log('mouse down. Contains: ' + ref.current.contains(event.target as Node));
for (const [_idx, dataElement] of data.current) {
if (!dataElement.ref.current) return;
if (dataElement.ref.current.contains(event.target as Node)) {
dataElement.mouseDownTarget = event.target as Node;
} else {
mouseRef.current.mouseDownTarget = null;
dataElement.mouseDownTarget = null;
}
},
[ref, mouseRef],
);
}
}, []);
const handleMouseUp = useCallback(
(event: MouseEvent) => {
if (!ref.current) return;
//console.log('mouse up. Contains: ' + ref.current.contains(event.target as Node));
if (ref.current.contains(event.target as Node)) {
mouseRef.current.mouseUpTarget = event.target as Node;
const handleMouseUp = useCallback((event: MouseEvent) => {
//console.log('mouse up. Contains: ' + ref.current.contains(event.target as Node));
for (const [_idx, dataElement] of data.current) {
if (!dataElement.ref.current) return;
if (dataElement.ref.current.contains(event.target as Node)) {
dataElement.mouseUpTarget = event.target as Node;
} else {
mouseRef.current.mouseUpTarget = null;
dataElement.mouseUpTarget = null;
}
},
[ref, mouseRef],
);
}
}, []);
const handleContextMenu = useCallback(
(event: MouseEvent) => {
if (!ref.current) return;
if (ref.current.contains(event.target as Node)) return;
for (const [_idx, { ref }] of data.current) {
if (!ref.current) return;
if (ref.current.contains(event.target as Node)) return;
}
// context menu is fired on mouse-down so no need to do special checks.
actionFunc();
},
[actionFunc, ref],
[actionFunc],
);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (!ref.current) return;
for (const [_idx, { ref }] of data.current) {
if (!ref.current) return;
}
if (event.key !== 'Escape') return;
actionFunc();
},
[actionFunc, ref],
[actionFunc],
);
useEffect(() => {