Refactor and fixes dropdown bugs (#8807)

Fixes https://github.com/twentyhq/twenty/issues/8788
Fixes https://github.com/twentyhq/twenty/issues/8793
Fixes https://github.com/twentyhq/twenty/issues/8791
Fixes https://github.com/twentyhq/twenty/issues/8890
Fixes https://github.com/twentyhq/twenty/issues/8893

- [x] Also : 

Icon buttons under dropdown are visible without blur : 

![Capture d’écran du 2024-11-29
15-09-53](https://github.com/user-attachments/assets/f563333d-4e43-4ded-acc7-62e116004ed9)

- [x] Also : 

<img width="237" alt="image"
src="https://github.com/user-attachments/assets/e4c70936-beff-4481-89cb-0a32a36e0ee2">

- [x] Also : 

<img width="335" alt="image"
src="https://github.com/user-attachments/assets/5be60395-6baf-49eb-8d40-197add049e20">

- [x] Also : 

<img width="287" alt="image"
src="https://github.com/user-attachments/assets/a317561f-7986-4d70-a1c0-deee4f4e268a">

- Button create new without padding
- Container is expanding

- [x] Also : 

<img width="303" alt="image"
src="https://github.com/user-attachments/assets/09f8a27f-91db-4191-acdc-aaaeedaf6da5">

- [x] Also : 

<img width="133" alt="image"
src="https://github.com/user-attachments/assets/fe17b32e-f7a4-46c4-8040-239eaf8198e8">

Font is cut at bottom ?

- [x] Also : 

<img width="385" alt="image"
src="https://github.com/user-attachments/assets/7bab2092-2936-4112-a2ee-d32d6737e304">

The component should flip and not resize in this situation

- [x] Also : 

<img width="244" alt="image"
src="https://github.com/user-attachments/assets/5384f49a-71f9-4638-a60c-158cc8c83f81">

- [x] Also : 


![image](https://github.com/user-attachments/assets/9cd1f43a-df59-401e-9a41-bdb8e93ebe58)
This commit is contained in:
Lucas Bordeau
2024-12-06 15:27:48 +01:00
committed by GitHub
parent 14b7bcf262
commit a9cb20f317
87 changed files with 1201 additions and 1192 deletions

View File

@ -1,12 +1,12 @@
import { fireEvent, renderHook } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { fireEvent, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import {
ClickOutsideMode,
useListenClickOutsideV2,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
useListenClickOutside,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
const containerRef = React.createRef<HTMLDivElement>();
@ -19,13 +19,13 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
);
const listenerId = 'listenerId';
describe('useListenClickOutsideV2', () => {
describe('useListenClickOutside', () => {
it('should trigger the callback when clicking outside the specified refs', () => {
const callback = jest.fn();
renderHook(
() =>
useListenClickOutsideV2({
useListenClickOutside({
refs: [containerRef],
callback,
listenerId,
@ -46,7 +46,7 @@ describe('useListenClickOutsideV2', () => {
renderHook(
() =>
useListenClickOutsideV2({
useListenClickOutside({
refs: [nullRef],
callback,
mode: ClickOutsideMode.comparePixels,
@ -68,7 +68,7 @@ describe('useListenClickOutsideV2', () => {
renderHook(
() =>
useListenClickOutsideV2({
useListenClickOutside({
refs: [containerRef],
callback,
listenerId,
@ -91,7 +91,7 @@ describe('useListenClickOutsideV2', () => {
renderHook(
() =>
useListenClickOutsideV2({
useListenClickOutside({
refs: [containerRef],
callback,
mode: ClickOutsideMode.comparePixels,

View File

@ -1,78 +0,0 @@
import { fireEvent, renderHook } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { isDefined } from '~/utils/isDefined';
import {
ClickOutsideMode,
useListenClickOutside,
} from '../useListenClickOutside';
const containerRef = React.createRef<HTMLDivElement>();
const nullRef = React.createRef<HTMLDivElement>();
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<div ref={containerRef}>{children}</div>
);
describe('useListenClickOutside', () => {
it('should trigger the callback when clicking outside the specified refs', () => {
const callback = jest.fn();
renderHook(
() => useListenClickOutside({ refs: [containerRef], callback }),
{ wrapper: Wrapper },
);
act(() => {
fireEvent.mouseDown(document);
fireEvent.click(document);
});
expect(callback).toHaveBeenCalled();
});
it('should not call the callback when clicking inside the specified refs using pixel comparison', () => {
const callback = jest.fn();
renderHook(
() =>
useListenClickOutside({
refs: [containerRef, nullRef],
callback,
mode: ClickOutsideMode.comparePixels,
}),
{ wrapper: Wrapper },
);
act(() => {
if (isDefined(containerRef.current)) {
fireEvent.mouseDown(containerRef.current);
fireEvent.click(containerRef.current);
}
});
expect(callback).not.toHaveBeenCalled();
});
it('should call the callback when clicking outside the specified refs using pixel comparison', () => {
const callback = jest.fn();
renderHook(() =>
useListenClickOutside({
refs: [containerRef, nullRef],
callback,
mode: ClickOutsideMode.comparePixels,
}),
);
act(() => {
// Simulate a click outside the specified refs
fireEvent.mouseDown(document.body);
fireEvent.click(document.body);
});
expect(callback).toHaveBeenCalled();
});
});

View File

@ -2,10 +2,7 @@ import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import {
ClickOutsideListenerProps,
useListenClickOutsideV2,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback';
import { toSpliced } from '~/utils/array/toSpliced';
import { isDefined } from '~/utils/isDefined';
@ -17,35 +14,6 @@ export const useClickOutsideListener = (componentId: string) => {
getClickOutsideListenerMouseDownHappenedState,
} = useClickOustideListenerStates(componentId);
const useListenClickOutside = <T extends Element>({
callback,
refs,
enabled,
mode,
}: Omit<ClickOutsideListenerProps<T>, 'listenerId'>) => {
return useListenClickOutsideV2({
listenerId: componentId,
refs,
callback: useRecoilCallback(
({ snapshot }) =>
(event) => {
callback(event);
const additionalCallbacks = snapshot
.getLoadable(getClickOutsideListenerCallbacksState)
.getValue();
additionalCallbacks.forEach((additionalCallback) => {
additionalCallback.callbackFunction(event);
});
},
[callback],
),
enabled,
mode,
});
};
const toggleClickOutsideListener = useRecoilCallback(
({ set }) =>
(activated: boolean) => {
@ -152,7 +120,6 @@ export const useClickOutsideListener = (componentId: string) => {
};
return {
useListenClickOutside,
toggleClickOutsideListener,
useRegisterClickOutsideListenerCallback,
};

View File

@ -1,140 +1,266 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
export enum ClickOutsideMode {
comparePixels = 'comparePixels',
compareHTMLRef = 'compareHTMLRef',
}
export const useListenClickOutside = <T extends Element>({
refs,
callback,
mode = ClickOutsideMode.compareHTMLRef,
enabled = true,
}: {
export type ClickOutsideListenerProps<T extends Element> = {
refs: Array<React.RefObject<T>>;
excludeClassNames?: string[];
callback: (event: MouseEvent | TouchEvent) => void;
mode?: ClickOutsideMode;
listenerId: string;
hotkeyScope?: string;
enabled?: boolean;
}) => {
const [isMouseDownInside, setIsMouseDownInside] = useState(false);
};
export const useListenClickOutside = <T extends Element>({
refs,
excludeClassNames,
callback,
mode = ClickOutsideMode.compareHTMLRef,
listenerId,
hotkeyScope,
enabled = true,
}: ClickOutsideListenerProps<T>) => {
const {
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
} = useClickOustideListenerStates(listenerId);
const handleMouseDown = useRecoilCallback(
({ snapshot, set }) =>
(event: MouseEvent | TouchEvent) => {
const clickOutsideListenerIsActivated = snapshot
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();
set(getClickOutsideListenerMouseDownHappenedState, true);
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.getValue();
const isListeningBasedOnHotkeyScope = hotkeyScope
? currentHotkeyScopes.includes(hotkeyScope)
: true;
const isListening =
clickOutsideListenerIsActivated &&
enabled &&
isListeningBasedOnHotkeyScope;
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,
);
}
},
[
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
hotkeyScope,
enabled,
mode,
refs,
getClickOutsideListenerIsMouseDownInsideState,
],
);
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
(event: MouseEvent | TouchEvent) => {
const clickOutsideListenerIsActivated = snapshot
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.getValue();
const isListeningBasedOnHotkeyScope = hotkeyScope
? currentHotkeyScopes.includes(hotkeyScope)
: true;
const isListening =
clickOutsideListenerIsActivated &&
enabled &&
isListeningBasedOnHotkeyScope;
const isMouseDownInside = snapshot
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
.getValue();
const hasMouseDownHappened = snapshot
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
.getValue();
if (mode === ClickOutsideMode.compareHTMLRef) {
const clickedElement = event.target as HTMLElement;
let isClickedOnExcluded = false;
let currentElement: HTMLElement | null = clickedElement;
while (currentElement) {
const currentClassList = currentElement.classList;
isClickedOnExcluded =
excludeClassNames?.some((className) =>
currentClassList.contains(className),
) ?? false;
if (isClickedOnExcluded) {
break;
}
currentElement = currentElement.parentElement;
}
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
if (
isListening &&
hasMouseDownHappened &&
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
!isClickedOnExcluded
) {
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 &&
isListening &&
hasMouseDownHappened
) {
callback(event);
}
}
},
[
getClickOutsideListenerIsActivatedState,
hotkeyScope,
enabled,
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerMouseDownHappenedState,
mode,
refs,
excludeClassNames,
callback,
],
);
useEffect(() => {
const handleMouseDown = (event: MouseEvent | TouchEvent) => {
if (mode === ClickOutsideMode.compareHTMLRef) {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
document.addEventListener('mousedown', handleMouseDown, {
capture: true,
});
document.addEventListener('click', handleClickOutside, { capture: true });
document.addEventListener('touchstart', handleMouseDown, {
capture: true,
});
document.addEventListener('touchend', handleClickOutside, {
capture: true,
});
setIsMouseDownInside(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;
});
setIsMouseDownInside(clickedOnAtLeastOneRef);
}
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,
});
};
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
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);
}
}
};
if (enabled) {
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, enabled, isMouseDownInside]);
}, [refs, callback, mode, handleClickOutside, handleMouseDown]);
};

View File

@ -1,266 +0,0 @@
import React, { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
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>>;
excludeClassNames?: string[];
callback: (event: MouseEvent | TouchEvent) => void;
mode?: ClickOutsideMode;
listenerId: string;
hotkeyScope?: string;
enabled?: boolean;
};
export const useListenClickOutsideV2 = <T extends Element>({
refs,
excludeClassNames,
callback,
mode = ClickOutsideMode.compareHTMLRef,
listenerId,
hotkeyScope,
enabled = true,
}: ClickOutsideListenerProps<T>) => {
const {
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
} = useClickOustideListenerStates(listenerId);
const handleMouseDown = useRecoilCallback(
({ snapshot, set }) =>
(event: MouseEvent | TouchEvent) => {
const clickOutsideListenerIsActivated = snapshot
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();
set(getClickOutsideListenerMouseDownHappenedState, true);
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.getValue();
const isListeningBasedOnHotkeyScope = hotkeyScope
? currentHotkeyScopes.includes(hotkeyScope)
: true;
const isListening =
clickOutsideListenerIsActivated &&
enabled &&
isListeningBasedOnHotkeyScope;
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,
);
}
},
[
getClickOutsideListenerIsActivatedState,
getClickOutsideListenerMouseDownHappenedState,
hotkeyScope,
enabled,
mode,
refs,
getClickOutsideListenerIsMouseDownInsideState,
],
);
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
(event: MouseEvent | TouchEvent) => {
const clickOutsideListenerIsActivated = snapshot
.getLoadable(getClickOutsideListenerIsActivatedState)
.getValue();
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.getValue();
const isListeningBasedOnHotkeyScope = hotkeyScope
? currentHotkeyScopes.includes(hotkeyScope)
: true;
const isListening =
clickOutsideListenerIsActivated &&
enabled &&
isListeningBasedOnHotkeyScope;
const isMouseDownInside = snapshot
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
.getValue();
const hasMouseDownHappened = snapshot
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
.getValue();
if (mode === ClickOutsideMode.compareHTMLRef) {
const clickedElement = event.target as HTMLElement;
let isClickedOnExcluded = false;
let currentElement: HTMLElement | null = clickedElement;
while (currentElement) {
const currentClassList = currentElement.classList;
isClickedOnExcluded =
excludeClassNames?.some((className) =>
currentClassList.contains(className),
) ?? false;
if (isClickedOnExcluded) {
break;
}
currentElement = currentElement.parentElement;
}
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
if (
isListening &&
hasMouseDownHappened &&
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
!isClickedOnExcluded
) {
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 &&
isListening &&
hasMouseDownHappened
) {
callback(event);
}
}
},
[
getClickOutsideListenerIsActivatedState,
hotkeyScope,
enabled,
getClickOutsideListenerIsMouseDownInsideState,
getClickOutsideListenerMouseDownHappenedState,
mode,
refs,
excludeClassNames,
callback,
],
);
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]);
};