Implemented useListenClickOutside V2 (#3507)
* Implemented useListenClickOutside V2 * Removed lock and implemented a toggle instead
This commit is contained in:
@ -1,4 +1,8 @@
|
|||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordClickOutsideListenerId';
|
||||||
|
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||||
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
|
|
||||||
export const MultipleObjectRecordOnClickOutsideEffect = ({
|
export const MultipleObjectRecordOnClickOutsideEffect = ({
|
||||||
containerRef,
|
containerRef,
|
||||||
@ -7,6 +11,21 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({
|
|||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
onClickOutside: () => void;
|
onClickOutside: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { useListenClickOutside } = useClickOutsideListener(
|
||||||
|
MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
|
||||||
|
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toggleRightDrawerClickOustideListener(false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
toggleRightDrawerClickOustideListener(true);
|
||||||
|
};
|
||||||
|
}, [toggleRightDrawerClickOustideListener]);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [containerRef],
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export const MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID =
|
||||||
|
'multi-object-record-click-outside-listener';
|
||||||
@ -5,11 +5,10 @@ import { motion } from 'framer-motion';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
|
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import {
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
ClickOutsideMode,
|
import { ClickOutsideMode } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
useListenClickOutside,
|
|
||||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -53,6 +52,10 @@ export const RightDrawer = () => {
|
|||||||
|
|
||||||
const rightDrawerRef = useRef<HTMLDivElement>(null);
|
const rightDrawerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { useListenClickOutside } = useClickOutsideListener(
|
||||||
|
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||||
|
);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [rightDrawerRef],
|
refs: [rightDrawerRef],
|
||||||
callback: () => closeRightDrawer(),
|
callback: () => closeRightDrawer(),
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export const RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID =
|
||||||
|
'right-drawer-click-outside-listener';
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { clickOutsideListenerIsActivatedStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedStateScopeMap';
|
||||||
|
import { clickOutsideListenerIsMouseDownInsideStateScopeMap } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideStateScopeMap';
|
||||||
|
import { lockedListenerIdState } from '@/ui/utilities/pointer-event/states/lockedListenerIdState';
|
||||||
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
import { getState } from '@/ui/utilities/recoil-scope/utils/getState';
|
||||||
|
|
||||||
|
export const useClickOustideListenerStates = (componentId: string) => {
|
||||||
|
// TODO: improve typing
|
||||||
|
const scopeId = getScopeIdFromComponentId(componentId) ?? '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
scopeId,
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState: getState(
|
||||||
|
clickOutsideListenerIsMouseDownInsideStateScopeMap,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
|
getClickOutsideListenerIsActivatedState: getState(
|
||||||
|
clickOutsideListenerIsActivatedStateScopeMap,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
|
lockedListenerIdState,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||||
|
import {
|
||||||
|
ClickOutsideListenerProps,
|
||||||
|
useListenClickOutsideV2,
|
||||||
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
||||||
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
|
||||||
|
export const useClickOutsideListener = (componentId: string) => {
|
||||||
|
// TODO: improve typing
|
||||||
|
const scopeId = getScopeIdFromComponentId(componentId) ?? '';
|
||||||
|
|
||||||
|
const { getClickOutsideListenerIsActivatedState } =
|
||||||
|
useClickOustideListenerStates(componentId);
|
||||||
|
|
||||||
|
const useListenClickOutside = <T extends Element>({
|
||||||
|
callback,
|
||||||
|
refs,
|
||||||
|
enabled,
|
||||||
|
mode,
|
||||||
|
}: Omit<ClickOutsideListenerProps<T>, 'listenerId'>) => {
|
||||||
|
return useListenClickOutsideV2({
|
||||||
|
listenerId: componentId,
|
||||||
|
refs,
|
||||||
|
callback,
|
||||||
|
enabled,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleClickOutsideListener = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
(activated: boolean) => {
|
||||||
|
set(getClickOutsideListenerIsActivatedState(), activated);
|
||||||
|
},
|
||||||
|
[getClickOutsideListenerIsActivatedState],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
scopeId,
|
||||||
|
useListenClickOutside,
|
||||||
|
toggleClickOutsideListener,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,184 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||||
|
|
||||||
|
export enum ClickOutsideMode {
|
||||||
|
comparePixels = 'comparePixels',
|
||||||
|
compareHTMLRef = 'compareHTMLRef',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClickOutsideListenerProps<T extends Element> = {
|
||||||
|
refs: Array<React.RefObject<T>>;
|
||||||
|
callback: (event: MouseEvent | TouchEvent) => void;
|
||||||
|
mode?: ClickOutsideMode;
|
||||||
|
listenerId: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListenClickOutsideV2 = <T extends Element>({
|
||||||
|
refs,
|
||||||
|
callback,
|
||||||
|
mode = ClickOutsideMode.compareHTMLRef,
|
||||||
|
listenerId,
|
||||||
|
enabled = true,
|
||||||
|
}: ClickOutsideListenerProps<T>) => {
|
||||||
|
const {
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
getClickOutsideListenerIsActivatedState,
|
||||||
|
} = useClickOustideListenerStates(listenerId);
|
||||||
|
|
||||||
|
const handleMouseDown = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const clickOutsideListenerIsActivated = snapshot
|
||||||
|
.getLoadable(getClickOutsideListenerIsActivatedState())
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const isListening = clickOutsideListenerIsActivated && enabled;
|
||||||
|
|
||||||
|
if (!isListening) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => ref.current?.contains(event.target as Node));
|
||||||
|
|
||||||
|
set(
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState(),
|
||||||
|
clickedOnAtLeastOneRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.comparePixels) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y, width, height } =
|
||||||
|
ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const clientX =
|
||||||
|
'clientX' in event
|
||||||
|
? event.clientX
|
||||||
|
: event.changedTouches[0].clientX;
|
||||||
|
const clientY =
|
||||||
|
'clientY' in event
|
||||||
|
? event.clientY
|
||||||
|
: event.changedTouches[0].clientY;
|
||||||
|
|
||||||
|
if (
|
||||||
|
clientX < x ||
|
||||||
|
clientX > x + width ||
|
||||||
|
clientY < y ||
|
||||||
|
clientY > y + height
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
set(
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState(),
|
||||||
|
clickedOnAtLeastOneRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
mode,
|
||||||
|
refs,
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
enabled,
|
||||||
|
getClickOutsideListenerIsActivatedState,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickOutside = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const isMouseDownInside = snapshot
|
||||||
|
.getLoadable(getClickOutsideListenerIsMouseDownInsideState())
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => ref.current?.contains(event.target as Node));
|
||||||
|
|
||||||
|
if (!clickedOnAtLeastOneRef && !isMouseDownInside) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.comparePixels) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y, width, height } =
|
||||||
|
ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const clientX =
|
||||||
|
'clientX' in event
|
||||||
|
? event.clientX
|
||||||
|
: event.changedTouches[0].clientX;
|
||||||
|
const clientY =
|
||||||
|
'clientY' in event
|
||||||
|
? event.clientY
|
||||||
|
: event.changedTouches[0].clientY;
|
||||||
|
|
||||||
|
if (
|
||||||
|
clientX < x ||
|
||||||
|
clientX > x + width ||
|
||||||
|
clientY < y ||
|
||||||
|
clientY > y + height
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clickedOnAtLeastOneRef && !isMouseDownInside) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mode, refs, callback, getClickOutsideListenerIsMouseDownInsideState],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleMouseDown, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
document.addEventListener('click', handleClickOutside, { capture: true });
|
||||||
|
document.addEventListener('touchstart', handleMouseDown, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
document.addEventListener('touchend', handleClickOutside, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleMouseDown, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
document.removeEventListener('click', handleClickOutside, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
document.removeEventListener('touchstart', handleMouseDown, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
document.removeEventListener('touchend', handleClickOutside, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [refs, callback, mode, handleClickOutside, handleMouseDown]);
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
|
||||||
|
|
||||||
|
export const clickOutsideListenerIsActivatedStateScopeMap =
|
||||||
|
createStateScopeMap<boolean>({
|
||||||
|
key: 'clickOutsideListenerIsActivatedStateScopeMap',
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
|
||||||
|
|
||||||
|
export const clickOutsideListenerIsMouseDownInsideStateScopeMap =
|
||||||
|
createStateScopeMap<boolean>({
|
||||||
|
key: 'clickOutsideListenerIsMouseDownInsideStateScopeMap',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const lockedListenerIdState = atom<string | null>({
|
||||||
|
key: 'lockedListenerIdState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user