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) { if (childRootRef) {
// this is alright to do since childRootRef (the ref itself) should never change for each component using this element // 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 // eslint-disable-next-line react-hooks/rules-of-hooks
useActionWhenEscapeOrClickedOrContextOutsideEffect(childRootRef, () => setOverlay(null)); useActionWhenEscapeOrClickedOrContextOutsideEffect([childRootRef], () => setOverlay(null));
} }
const keyDownHandler = useCallback( const keyDownHandler = useCallback(

View File

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

View File

@ -7,11 +7,19 @@ import React, { FC, useCallback, useMemo, useRef } from 'react';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
import ElementsUtil, { IAlignment } from '../require/elements-util'; import ElementsUtil, { IAlignment } from '../require/elements-util';
import DownloadButton from '../components/button-download'; 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 ImageContextMenu from '../contexts/context-menu-image';
import Overlay from '../components/overlay'; import Overlay from '../components/overlay';
import { useRecoilValue } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { guildResourceSoftImgSrcState, guildResourceState, useRecoilValueSoftImgSrc } from '../require/atoms'; import {
guildResourceSoftImgSrcState,
guildResourceState,
overlayState,
useRecoilValueSoftImgSrc,
} from '../require/atoms';
import { isFailed, isLoaded } from '../require/loadables'; import { isFailed, isLoaded } from '../require/loadables';
export interface ImageOverlayProps { export interface ImageOverlayProps {
@ -23,7 +31,11 @@ export interface ImageOverlayProps {
const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => { const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
const { guild, resourceId, resourceName } = props; const { guild, resourceId, resourceName } = props;
const setOverlay = useSetRecoilState(overlayState);
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const dltxtRef = useRef<HTMLDivElement>(null);
// NOTE: These will update together. How convenient! // NOTE: These will update together. How convenient!
const resource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId })); const resource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId }));
@ -46,13 +58,7 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
[resource, resourceName], [resource, resourceName],
); );
const onBaseClick = useCallback( useActionWhenEscapeOrClickedOrContextOutsideEffect([imgRef, dltxtRef], () => setOverlay(null));
(e: MouseEvent) => {
if (!rootRef.current) return 1;
return 2;
},
[rootRef],
);
const sizeText = useMemo(() => { const sizeText = useMemo(() => {
if (isFailed(resource)) return 'Failed to load'; if (isFailed(resource)) return 'Failed to load';
@ -61,10 +67,10 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
}, [resource]); }, [resource]);
return ( return (
<Overlay childRootRef={rootRef}> <Overlay>
<div ref={rootRef} className="content popup-image"> <div ref={rootRef} className="content popup-image">
<img src={imgSrc} alt={resourceName} title={resourceName} onContextMenu={onContextMenu} /> <img ref={imgRef} src={imgSrc} alt={resourceName} title={resourceName} onContextMenu={onContextMenu} />
<div className="download"> <div ref={dltxtRef} className="download">
<div className="info"> <div className="info">
<div className="name">{resourceName}</div> <div className="name">{resourceName}</div>
<div className="size">{sizeText}</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 // 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 */ /** calls the close action when you hit escape or click outside of the ref element */
export function useActionWhenEscapeOrClickedOrContextOutsideEffect( export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
ref: RefObject<HTMLElement>, refs: RefObject<HTMLElement>[],
actionFunc: () => void, actionFunc: () => void,
) { ) {
// have to use a ref here and not states since we can't re-assign state between mouseup and click // 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 }>({ const data = useRef<
mouseDownTarget: null, Map<number, { ref: RefObject<HTMLElement>; mouseDownTarget: Node | null; mouseUpTarget: Node | null }>
mouseUpTarget: 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( const handleClickOutside = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
//console.log('current:', ref.current, 'target:', event.target, 'mouseDownTarget:', mouseRef.current.mouseDownTarget, 'mouseUpTarget:', mouseRef.current.mouseUpTarget); //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 // casting here is OK. https://stackoverflow.com/q/61164018
if (ref.current.contains(event.target as Node)) return; 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(); actionFunc();
}, },
[ref, mouseRef, actionFunc], [actionFunc],
); );
const handleMouseDown = useCallback( const handleMouseDown = useCallback((event: MouseEvent) => {
(event: MouseEvent) => {
if (!ref.current) return;
//console.log('mouse down. Contains: ' + ref.current.contains(event.target as Node)); //console.log('mouse down. Contains: ' + ref.current.contains(event.target as Node));
if (ref.current.contains(event.target as Node)) { for (const [_idx, dataElement] of data.current) {
mouseRef.current.mouseDownTarget = event.target as Node; if (!dataElement.ref.current) return;
if (dataElement.ref.current.contains(event.target as Node)) {
dataElement.mouseDownTarget = event.target as Node;
} else { } else {
mouseRef.current.mouseDownTarget = null; dataElement.mouseDownTarget = null;
} }
}, }
[ref, mouseRef], }, []);
);
const handleMouseUp = useCallback( const handleMouseUp = useCallback((event: MouseEvent) => {
(event: MouseEvent) => {
if (!ref.current) return;
//console.log('mouse up. Contains: ' + ref.current.contains(event.target as Node)); //console.log('mouse up. Contains: ' + ref.current.contains(event.target as Node));
if (ref.current.contains(event.target as Node)) { for (const [_idx, dataElement] of data.current) {
mouseRef.current.mouseUpTarget = event.target as Node; if (!dataElement.ref.current) return;
if (dataElement.ref.current.contains(event.target as Node)) {
dataElement.mouseUpTarget = event.target as Node;
} else { } else {
mouseRef.current.mouseUpTarget = null; dataElement.mouseUpTarget = null;
} }
}, }
[ref, mouseRef], }, []);
);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
for (const [_idx, { ref }] of data.current) {
if (!ref.current) return; if (!ref.current) return;
if (ref.current.contains(event.target as Node)) return; if (ref.current.contains(event.target as Node)) return;
}
// context menu is fired on mouse-down so no need to do special checks. // context menu is fired on mouse-down so no need to do special checks.
actionFunc(); actionFunc();
}, },
[actionFunc, ref], [actionFunc],
); );
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
for (const [_idx, { ref }] of data.current) {
if (!ref.current) return; if (!ref.current) return;
}
if (event.key !== 'Escape') return; if (event.key !== 'Escape') return;
actionFunc(); actionFunc();
}, },
[actionFunc, ref], [actionFunc],
); );
useEffect(() => { useEffect(() => {