mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-23 14:03:35 +03:00
Implemented useListenClickOutside V2 (#3507)
* Implemented useListenClickOutside V2 * Removed lock and implemented a toggle instead
This commit is contained in:
parent
808100fdd5
commit
bbfe62df9a
@ -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 = ({
|
||||
containerRef,
|
||||
@ -7,6 +11,21 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
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({
|
||||
refs: [containerRef],
|
||||
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 { 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 {
|
||||
ClickOutsideMode,
|
||||
useListenClickOutside,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
import { ClickOutsideMode } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
@ -53,6 +52,10 @@ export const RightDrawer = () => {
|
||||
|
||||
const rightDrawerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { useListenClickOutside } = useClickOutsideListener(
|
||||
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [rightDrawerRef],
|
||||
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,
|
||||
});
|
Loading…
Reference in New Issue
Block a user