diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index a1bf0e32e..aaaf7405d 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -10,6 +10,7 @@ import { mockedUserJWT } from '../src/testing/mock-data/jwt'; import 'react-loading-skeleton/dist/skeleton.css'; import 'twenty-ui/style.css'; import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui/theme'; +import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext'; initialize({ onUnhandledRequest: async (request: Request) => { @@ -46,7 +47,11 @@ const preview: Preview = { return ( - + + + ); diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx index fbc5ed335..ff8af27e9 100644 --- a/packages/twenty-front/src/modules/app/components/App.tsx +++ b/packages/twenty-front/src/modules/app/components/App.tsx @@ -5,6 +5,7 @@ import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { AppRootErrorFallback } from '@/error-handler/components/AppRootErrorFallback'; import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext'; import { i18n } from '@lingui/core'; import { I18nProvider } from '@lingui/react'; import { HelmetProvider } from 'react-helmet-async'; @@ -28,7 +29,11 @@ export const App = () => { - + + + diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx index f2f6511c7..ed53e9127 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown.tsx @@ -1,4 +1,3 @@ -import styled from '@emotion/styled'; import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; @@ -35,10 +34,6 @@ type RecordDetailRelationSectionDropdownProps = { loading: boolean; }; -const StyledAddDropdown = styled(Dropdown)` - margin-left: auto; -`; - export const RecordDetailRelationSectionDropdown = ({ loading, }: RecordDetailRelationSectionDropdownProps) => { @@ -196,7 +191,7 @@ export const RecordDetailRelationSectionDropdown = ({ return ( - ; }; +const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` + z-index: ${({ theme }) => theme.lastLayerZIndex}; +`; + export const RecordTableColumnHeadDropdownMenu = ({ column, }: RecordTableColumnHeadDropdownMenuProps) => { @@ -102,7 +107,7 @@ export const RecordTableColumnHeadDropdownMenu = ({ const canHide = column.isLabelIdentifier !== true; return ( - + {isFilterable && ( )} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx index 769f57db6..4587ac49d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown.tsx @@ -2,7 +2,6 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper'; -import styled from '@emotion/styled'; import { useCallback } from 'react'; import { RecordTableColumnHead } from './RecordTableColumnHead'; import { RecordTableColumnHeadDropdownMenu } from './RecordTableColumnHeadDropdownMenu'; @@ -11,12 +10,6 @@ type RecordTableColumnHeadWithDropdownProps = { column: ColumnDefinition; }; -const StyledDropdown = styled(Dropdown)` - display: flex; - flex: 1; - z-index: ${({ theme }) => theme.lastLayerZIndex}; -`; - export const RecordTableColumnHeadWithDropdown = ({ column, }: RecordTableColumnHeadWithDropdownProps) => { @@ -34,7 +27,7 @@ export const RecordTableColumnHeadWithDropdown = ({ }, [toggleScrollXWrapper, toggleScrollYWrapper]); return ( - void; onClose?: () => void; onOpen?: () => void; - avoidPortal?: boolean; }; export const Dropdown = ({ - className, clickableComponent, dropdownComponents, dropdownWidth, @@ -74,7 +71,6 @@ export const Dropdown = ({ onClickOutside, onClose, onOpen, - avoidPortal, clickableComponentWidth = 'auto', }: DropdownProps) => { const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId); @@ -174,7 +170,6 @@ export const Dropdown = ({ )} {isDropdownOpen && ( )} { const { isDropdownOpen, closeDropdown, setDropdownPlacement } = useDropdown(dropdownId); @@ -121,30 +118,16 @@ export const DropdownContent = ({ maxWidth: dropdownMaxWidth, }; + const { excludeClassName } = useContext(ClickOutsideListenerContext); + return ( <> {hotkey && onHotkeyTriggered && ( )} - {avoidPortal ? ( - - - - {dropdownComponents} - - - - ) : ( - + + +
@@ -162,8 +144,8 @@ export const DropdownContent = ({ - - )} +
+
); }; diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx index f8735536e..a956a4447 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx @@ -4,7 +4,9 @@ import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkey import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext'; import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; +import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { css, useTheme } from '@emotion/react'; @@ -225,36 +227,43 @@ export const Modal = ({ instanceId: modalId, }} > - - - + - {children} - - + + {children} + + + )} diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx index 055bcda32..cbb9e649d 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx @@ -1,9 +1,7 @@ import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope'; +import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { - ClickOutsideMode, - useListenClickOutside, -} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { Key } from 'ts-key-enum'; type ModalHotkeysAndClickOutsideEffectProps = { @@ -41,13 +39,13 @@ export const ModalHotkeysAndClickOutsideEffect = ({ useListenClickOutside({ refs: [modalRef], + excludeClassNames: [MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME], listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`, callback: () => { if (isClosable && onClose !== undefined) { onClose(); } }, - mode: ClickOutsideMode.comparePixels, }); return null; diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx index fcf6abea3..478f13a40 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx @@ -130,51 +130,51 @@ export const ConfirmWithEnterKey: Story = { }, }; -// export const CancelButtonClick: Story = { -// args: { -// modalId: 'confirmation-modal', -// title: 'Cancel Button Test', -// subtitle: 'Clicking the cancel button should close the modal', -// confirmButtonText: 'Confirm', -// onClose: closeMock, -// }, -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); +export const CancelButtonClick: Story = { + args: { + modalId: 'confirmation-modal', + title: 'Cancel Button Test', + subtitle: 'Clicking the cancel button should close the modal', + confirmButtonText: 'Confirm', + onClose: closeMock, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); -// await canvas.findByText('Cancel Button Test'); + await canvas.findByText('Cancel Button Test'); -// const cancelButton = await canvas.findByRole('button', { -// name: /Cancel/, -// }); -// await userEvent.click(cancelButton); + const cancelButton = await canvas.findByRole('button', { + name: /Cancel/, + }); + await userEvent.click(cancelButton); -// await waitFor(() => { -// expect(closeMock).toHaveBeenCalledTimes(1); -// }); -// }, -// }; + await waitFor(() => { + expect(closeMock).toHaveBeenCalledTimes(1); + }); + }, +}; -// export const ConfirmButtonClick: Story = { -// args: { -// modalId: 'confirmation-modal', -// title: 'Confirm Button Test', -// subtitle: 'Clicking the confirm button should trigger the confirm action', -// confirmButtonText: 'Confirm', -// onConfirmClick: confirmMock, -// }, -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); +export const ConfirmButtonClick: Story = { + args: { + modalId: 'confirmation-modal', + title: 'Confirm Button Test', + subtitle: 'Clicking the confirm button should trigger the confirm action', + confirmButtonText: 'Confirm', + onConfirmClick: confirmMock, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); -// await canvas.findByText('Confirm Button Test'); + await canvas.findByText('Confirm Button Test'); -// const confirmButton = await canvas.findByRole('button', { -// name: /Confirm/, -// }); + const confirmButton = await canvas.findByRole('button', { + name: /Confirm/, + }); -// await userEvent.click(confirmButton); + await userEvent.click(confirmButton); -// await waitFor(() => { -// expect(confirmMock).toHaveBeenCalledTimes(1); -// }); -// }, -// }; + await waitFor(() => { + expect(confirmMock).toHaveBeenCalledTimes(1); + }); + }, +}; diff --git a/packages/twenty-front/src/modules/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName.ts b/packages/twenty-front/src/modules/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName.ts new file mode 100644 index 000000000..848b5a6ea --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName.ts @@ -0,0 +1,2 @@ +export const MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME = + 'modal-click-outside-listener-excluded'; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext.tsx new file mode 100644 index 000000000..a2bfb9f46 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext.tsx @@ -0,0 +1,10 @@ +import { createContext } from 'react'; + +type ClickOutsideListenerContextType = { + excludeClassName: string | undefined; +}; + +export const ClickOutsideListenerContext = + createContext({ + excludeClassName: undefined, + }); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx index 52dd5d064..e37dab118 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx @@ -1,16 +1,11 @@ import { fireEvent, renderHook } from '@testing-library/react'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; +import React, { act } from 'react'; import { RecoilRoot } from 'recoil'; -import { - ClickOutsideMode, - useListenClickOutside, -} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from 'twenty-shared/utils'; const containerRef = React.createRef(); -const nullRef = React.createRef(); const Wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -41,28 +36,6 @@ describe('useListenClickOutside', () => { expect(callback).toHaveBeenCalled(); }); - it('should trigger the callback when clicking outside the specified ref with pixel comparison', async () => { - const callback = jest.fn(); - - renderHook( - () => - useListenClickOutside({ - refs: [nullRef], - callback, - mode: ClickOutsideMode.comparePixels, - listenerId, - }), - { 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 default comparison', () => { const callback = jest.fn(); @@ -85,28 +58,4 @@ describe('useListenClickOutside', () => { expect(callback).not.toHaveBeenCalled(); }); - - it('should not call the callback when clicking inside the specified refs using pixel comparison', () => { - const callback = jest.fn(); - - renderHook( - () => - useListenClickOutside({ - refs: [containerRef], - callback, - mode: ClickOutsideMode.comparePixels, - listenerId, - }), - { wrapper: Wrapper }, - ); - - act(() => { - if (isDefined(containerRef.current)) { - fireEvent.mouseDown(containerRef.current); - fireEvent.click(containerRef.current); - } - }); - - expect(callback).not.toHaveBeenCalled(); - }); }); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts index 57d76dd1e..371b2a397 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts @@ -7,16 +7,10 @@ import { useRecoilCallback } from 'recoil'; const CLICK_OUTSIDE_DEBUG_MODE = false; -export enum ClickOutsideMode { - comparePixels = 'comparePixels', - compareHTMLRef = 'compareHTMLRef', -} - export type ClickOutsideListenerProps = { refs: Array>; excludeClassNames?: string[]; callback: (event: MouseEvent | TouchEvent) => void; - mode?: ClickOutsideMode; listenerId: string; enabled?: boolean; }; @@ -25,7 +19,6 @@ export const useListenClickOutside = ({ refs, excludeClassNames, callback, - mode = ClickOutsideMode.compareHTMLRef, listenerId, enabled = true, }: ClickOutsideListenerProps) => { @@ -60,67 +53,16 @@ export const useListenClickOutside = ({ return; } - switch (mode) { - case ClickOutsideMode.compareHTMLRef: { - const clickedOnAtLeastOneRef = refs - .filter((ref) => !!ref.current) - .some((ref) => ref.current?.contains(event.target as Node)); + const clickedOnAtLeastOneRef = refs + .filter((ref) => !!ref.current) + .some((ref) => ref.current?.contains(event.target as Node)); - set( - clickOutsideListenerIsMouseDownInsideState, - clickedOnAtLeastOneRef, - ); - break; - } - - case 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( - clickOutsideListenerIsMouseDownInsideState, - clickedOnAtLeastOneRef, - ); - break; - } - - default: { - break; - } - } + set(clickOutsideListenerIsMouseDownInsideState, clickedOnAtLeastOneRef); }, [ clickOutsideListenerIsActivatedState, clickOutsideListenerMouseDownHappenedState, enabled, - mode, refs, clickOutsideListenerIsMouseDownInsideState, ], @@ -162,94 +104,34 @@ export const useListenClickOutside = ({ currentElement = currentElement.parentElement; } - if (mode === ClickOutsideMode.compareHTMLRef) { - const clickedOnAtLeastOneRef = refs - .filter((ref) => !!ref.current) - .some((ref) => ref.current?.contains(event.target as Node)); + const clickedOnAtLeastOneRef = refs + .filter((ref) => !!ref.current) + .some((ref) => ref.current?.contains(event.target as Node)); - const shouldTrigger = - isListening && - hasMouseDownHappened && - !clickedOnAtLeastOneRef && - !isMouseDownInside && - !isClickedOnExcluded; + const shouldTrigger = + isListening && + hasMouseDownHappened && + !clickedOnAtLeastOneRef && + !isMouseDownInside && + !isClickedOnExcluded; - if (CLICK_OUTSIDE_DEBUG_MODE) { - // eslint-disable-next-line no-console - console.log('click outside compare ref', { - listenerId, - shouldTrigger, - clickedOnAtLeastOneRef, - isMouseDownInside, - isListening, - hasMouseDownHappened, - isClickedOnExcluded, - enabled, - event, - }); - } - - if (shouldTrigger) { - callback(event); - } + if (CLICK_OUTSIDE_DEBUG_MODE) { + // eslint-disable-next-line no-console + console.log('click outside compare ref', { + listenerId, + shouldTrigger, + clickedOnAtLeastOneRef, + isMouseDownInside, + isListening, + hasMouseDownHappened, + isClickedOnExcluded, + enabled, + 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; - }); - - const shouldTrigger = - !clickedOnAtLeastOneRef && - !isMouseDownInside && - isListening && - hasMouseDownHappened && - !isClickedOnExcluded; - - if (CLICK_OUTSIDE_DEBUG_MODE) { - // eslint-disable-next-line no-console - console.log('click outside compare pixel', { - listenerId, - shouldTrigger, - clickedOnAtLeastOneRef, - isMouseDownInside, - isListening, - hasMouseDownHappened, - isClickedOnExcluded, - enabled, - event, - }); - } - - if (shouldTrigger) { - callback(event); - } + if (shouldTrigger) { + callback(event); } }, [ @@ -257,7 +139,6 @@ export const useListenClickOutside = ({ enabled, clickOutsideListenerIsMouseDownInsideState, clickOutsideListenerMouseDownHappenedState, - mode, refs, excludeClassNames, callback, @@ -291,5 +172,5 @@ export const useListenClickOutside = ({ capture: true, }); }; - }, [refs, callback, mode, handleClickOutside, handleMouseDown]); + }, [refs, callback, handleClickOutside, handleMouseDown]); }; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx index fd4822e6f..450417761 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdown.tsx @@ -51,7 +51,6 @@ export const ViewBarFilterDropdown = ({ dropdownHotkeyScope={hotkeyScope} dropdownOffset={{ y: 8 }} onClickOutside={handleDropdownClickOutside} - dropdownWidth={280} /> ); }; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx index 1eb0dd3bd..1d2e7d46b 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx @@ -79,7 +79,7 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => { selectableItemIdArray={selectableFieldMetadataItemIds} selectableListInstanceId={FILTER_FIELD_LIST_ID} > - + {selectableVisibleFieldMetadataItems.map( (visibleFieldMetadataItem) => (