diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index b1fe1e8e2..2113798b7 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; +import { gql } from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx index a3ef043c6..c319f04e1 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx @@ -14,7 +14,12 @@ import { } from '@/action-menu/types/ActionMenuEntry'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; -import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui'; +import { + IconCheckbox, + IconHeart, + IconTrash, + getCanvasElementForDropdownTesting, +} from 'twenty-ui'; const deleteMock = jest.fn(); const markAsDoneMock = jest.fn(); @@ -107,7 +112,9 @@ export const WithInteractions: Story = { args: { actionMenuId: 'story', }, - play: async ({ canvasElement }) => { + play: async () => { + const canvasElement = getCanvasElementForDropdownTesting(); + const canvas = within(canvasElement); const deleteButton = await canvas.findByText('Delete'); diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RightDrawerActionMenuDropdown.stories.tsx similarity index 97% rename from packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx rename to packages/twenty-front/src/modules/action-menu/components/__stories__/RightDrawerActionMenuDropdown.stories.tsx index 413ee1683..a907f8fbf 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RightDrawerActionMenuDropdown.stories.tsx @@ -15,6 +15,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s import { userEvent, waitFor, within } from '@storybook/test'; import { ComponentDecorator, + getCanvasElementForDropdownTesting, IconFileExport, IconHeart, IconTrash, @@ -117,8 +118,8 @@ export const WithButtonClicks: Story = { args: { actionMenuId: 'story-action-menu', }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); + play: async () => { + const canvas = within(getCanvasElementForDropdownTesting()); let actionButton = await canvas.findByText('Actions'); await userEvent.click(actionButton); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 887110007..e79932e4a 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -32,7 +32,7 @@ import { SelectableItem } from '@/ui/layout/selectable-list/components/Selectabl import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; -import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -414,7 +414,7 @@ export const CommandMenu = () => { cmd.scope === CommandScope.Global, ); - useListenClickOutsideV2({ + useListenClickOutside({ refs: [commandMenuRef], callback: closeCommandMenu, listenerId: 'COMMAND_MENU_LISTENER_ID', diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index 906661a66..d879d35f6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -16,7 +16,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; @@ -167,43 +166,41 @@ export const ObjectFilterDropdownFilterSelect = ({ setObjectFilterDropdownSearchInput(event.target.value) } /> - - - - {visibleColumnsFilterDefinitions.map( - (visibleFilterDefinition, index) => ( - - - - ), - )} - {shoudShowSeparator && } - {hiddenColumnsFilterDefinitions.map( - (hiddenFilterDefinition, index) => ( - - - - ), - )} - - - {shouldShowAdvancedFilterButton && } - + + + {visibleColumnsFilterDefinitions.map( + (visibleFilterDefinition, index) => ( + + + + ), + )} + {shoudShowSeparator && } + {hiddenColumnsFilterDefinitions.map( + (hiddenFilterDefinition, index) => ( + + + + ), + )} + + + {shouldShowAdvancedFilterButton && } ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx index 5137802e7..840d87ab9 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx @@ -28,9 +28,11 @@ export const ObjectFilterDropdownOperandDropdown = ({ const theme = useTheme(); + const dropdownId = `${filterDropdownId}-operand-dropdown`; + return ( { } }} > - - - {optionsInDropdown?.map((option) => ( - - handleMultipleOptionSelectChange(option, selected) - } - text={option.label} - color={option.color} - className="" - /> - ))} - - + + {optionsInDropdown?.map((option) => ( + + handleMultipleOptionSelectChange(option, selected) + } + text={option.label} + color={option.color} + className="" + /> + ))} + {showNoResult && } ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx index 16d060c53..90d85da61 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx @@ -11,7 +11,10 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { within } from '@storybook/test'; -import { ComponentDecorator } from 'twenty-ui'; +import { + ComponentDecorator, + getCanvasElementForDropdownTesting, +} from 'twenty-ui'; import { FieldMetadataType } from '~/generated/graphql'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; @@ -120,7 +123,7 @@ type Story = StoryObj; export const Default: Story = { play: async () => { - const canvas = within(document.body); + const canvas = within(getCanvasElementForDropdownTesting()); const filterButton = await canvas.findByText('Filter'); @@ -142,7 +145,7 @@ export const Default: Story = { export const Date: Story = { play: async () => { - const canvas = within(document.body); + const canvas = within(getCanvasElementForDropdownTesting()); const filterButton = await canvas.findByText('Filter'); @@ -156,7 +159,7 @@ export const Date: Story = { export const Number: Story = { play: async () => { - const canvas = within(document.body); + const canvas = within(getCanvasElementForDropdownTesting()); const filterButton = await canvas.findByText('Filter'); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx index c6063a7f5..69eda6e55 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx @@ -6,7 +6,6 @@ import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hook import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; import { ViewType } from '@/views/types/ViewType'; @@ -53,19 +52,17 @@ export const ObjectOptionsDropdownFieldsContent = () => { Fields - - - + - + onContentChange('hiddenFields')} LeftIcon={IconEyeOff} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx index c19550505..15a87ed2f 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent.tsx @@ -18,7 +18,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; import { ViewType } from '@/views/types/ViewType'; @@ -71,19 +70,16 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => { Hidden Fields {hiddenRecordFields.length > 0 && ( - - - + )} - { @@ -91,7 +87,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => { closeDropdown(); }} > - + diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index 52fd645bf..ebad9ac41 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -15,7 +15,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useContext } from 'react'; import { SORT_DIRECTIONS } from '../types/SortDirection'; @@ -184,39 +183,37 @@ export const ObjectSortDropdownButton = ({ setObjectSortDropdownSearchInput(event.target.value) } /> - - - {visibleColumnsSortDefinitions.map( - (visibleSortDefinition, index) => ( - { - setObjectSortDropdownSearchInput(''); - handleAddSort(visibleSortDefinition); - }} - LeftIcon={getIcon(visibleSortDefinition.iconName)} - text={visibleSortDefinition.label} - /> - ), - )} - {shoudShowSeparator && } - {hiddenColumnsSortDefinitions.map( - (hiddenSortDefinition, index) => ( - { - setObjectSortDropdownSearchInput(''); - handleAddSort(hiddenSortDefinition); - }} - LeftIcon={getIcon(hiddenSortDefinition.iconName)} - text={hiddenSortDefinition.label} - /> - ), - )} - - + + {visibleColumnsSortDefinitions.map( + (visibleSortDefinition, index) => ( + { + setObjectSortDropdownSearchInput(''); + handleAddSort(visibleSortDefinition); + }} + LeftIcon={getIcon(visibleSortDefinition.iconName)} + text={visibleSortDefinition.label} + /> + ), + )} + {shoudShowSeparator && } + {hiddenColumnsSortDefinitions.map( + (hiddenSortDefinition, index) => ( + { + setObjectSortDropdownSearchInput(''); + handleAddSort(hiddenSortDefinition); + }} + LeftIcon={getIcon(hiddenSortDefinition.iconName)} + text={hiddenSortDefinition.label} + /> + ), + )} + } onClose={handleDropdownButtonClose} diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 897a9fd91..f7fee7566 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -22,7 +22,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -81,7 +81,7 @@ export const RecordBoard = () => { const { resetRecordSelection, setRecordAsSelected } = useRecordBoardSelection(recordBoardId); - useListenClickOutsideV2({ + useListenClickOutside({ excludeClassNames: [ 'bottom-bar', 'action-menu-dropdown', diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx index bfbfb7e9d..b4caeb7c3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx @@ -34,6 +34,7 @@ export const RecordBoardColumnDropdownMenu = ({ useListenClickOutside({ refs: [boardColumnMenuRef], callback: closeMenu, + listenerId: 'record-board-column-dropdown-menu', }); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 1f36c3454..723721b0b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -48,7 +48,10 @@ type FieldInputProps = { recordFieldInputdId: string; onSubmit?: FieldInputEvent; onCancel?: () => void; - onClickOutside?: FieldInputEvent; + onClickOutside?: ( + persist: () => void, + event: MouseEvent | TouchEvent, + ) => void; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onTab?: FieldInputEvent; @@ -78,7 +81,10 @@ export const FieldInput = ({ ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( ) : isFieldPhones(fieldDefinition) ? ( - + onClickOutside?.(() => {}, event)} + /> ) : isFieldText(fieldDefinition) ? ( ) : isFieldEmails(fieldDefinition) ? ( - + onClickOutside?.(() => {}, event)} + /> ) : isFieldFullName(fieldDefinition) ? ( ) : isFieldLinks(fieldDefinition) ? ( - + onClickOutside?.(() => {}, event)} + /> ) : isFieldCurrency(fieldDefinition) ? ( ) : isFieldArray(fieldDefinition) ? ( - + onClickOutside?.(() => {}, event)} + /> ) : ( <> )} diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx index 1be174704..9a22d787f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx @@ -108,9 +108,6 @@ export const FormSelectFieldInput = ({ goBackToPreviousHotkeyScope(); }; - const [selectWrapperRef, setSelectWrapperRef] = - useState(null); - const [filteredOptions, setFilteredOptions] = useState([]); const { resetSelectedItem } = useSelectableList( @@ -206,7 +203,6 @@ export const FormSelectFieldInput = ({ {draftValue.type === 'static' ? ( @@ -231,7 +227,6 @@ export const FormSelectFieldInput = ({ selectableItemIdArray={optionIds} hotkeyScope={hotkeyScope} onEnter={handleSelectEnter} - selectWrapperRef={selectWrapperRef} onOptionSelected={handleSubmit} options={field.metadata.options} onCancel={onCancel} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx index faafb52df..51faca35e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/AddressFieldInput.tsx @@ -4,10 +4,13 @@ import { AddressInput } from '@/ui/field/input/components/AddressInput'; import { usePersistField } from '../../../hooks/usePersistField'; -import { FieldInputEvent } from './DateTimeFieldInput'; +import { + FieldInputClickOutsideEvent, + FieldInputEvent, +} from './DateTimeFieldInput'; export type AddressFieldInputProps = { - onClickOutside?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onTab?: FieldInputEvent; @@ -60,7 +63,7 @@ export const AddressFieldInput = ({ event: MouseEvent | TouchEvent, newAddress: FieldAddressDraftValue, ) => { - onClickOutside?.(() => persistField(convertToAddress(newAddress))); + onClickOutside?.(() => persistField(convertToAddress(newAddress)), event); }; const handleChange = (newAddress: FieldAddressDraftValue) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx index 2353f7d2f..7b324d758 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/ArrayFieldInput.tsx @@ -6,9 +6,13 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; type ArrayFieldInputProps = { onCancel?: () => void; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; -export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => { +export const ArrayFieldInput = ({ + onCancel, + onClickOutside, +}: ArrayFieldInputProps) => { const { persistArrayField, hotkeyScope, fieldValue } = useArrayField(); const arrayItems = useMemo>( @@ -23,6 +27,7 @@ export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => { items={arrayItems} onPersist={persistArrayField} onCancel={onCancel} + onClickOutside={onClickOutside} placeholder="Enter value" fieldMetadataType={FieldMetadataType.Array} renderItem={({ value, index, handleEdit, handleDelete }) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx index 89a4e054e..2ca460fc8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx @@ -8,10 +8,13 @@ import { FieldInputOverlay } from '../../../../../ui/field/input/components/Fiel import { useCurrencyField } from '../../hooks/useCurrencyField'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { FieldInputEvent } from './DateTimeFieldInput'; +import { + FieldInputClickOutsideEvent, + FieldInputEvent, +} from './DateTimeFieldInput'; type CurrencyFieldInputProps = { - onClickOutside?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onTab?: FieldInputEvent; @@ -79,7 +82,7 @@ export const CurrencyFieldInput = ({ amountText: newValue, currencyCode, }); - }); + }, event); }; const handleTab = (newValue: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx index 244aa1f20..0e67f4baa 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx @@ -5,11 +5,12 @@ import { DateInput } from '@/ui/field/input/components/DateInput'; import { isDefined } from '~/utils/isDefined'; import { usePersistField } from '../../../hooks/usePersistField'; +import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; type FieldInputEvent = (persist: () => void) => void; type DateFieldInputProps = { - onClickOutside?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onClear?: FieldInputEvent; @@ -50,10 +51,10 @@ export const DateFieldInput = ({ }; const handleClickOutside = ( - _event: MouseEvent | TouchEvent, + event: MouseEvent | TouchEvent, newDate: Nullable, ) => { - onClickOutside?.(() => persistDate(newDate)); + onClickOutside?.(() => persistDate(newDate), event); }; const handleChange = (newDate: Nullable) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx index 6a5749008..15f85c61c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx @@ -6,9 +6,13 @@ import { usePersistField } from '../../../hooks/usePersistField'; import { useDateTimeField } from '../../hooks/useDateTimeField'; export type FieldInputEvent = (persist: () => void) => void; +export type FieldInputClickOutsideEvent = ( + persist: () => void, + event: MouseEvent | TouchEvent, +) => void; export type DateTimeFieldInputProps = { - onClickOutside?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onClear?: FieldInputEvent; @@ -45,10 +49,10 @@ export const DateTimeFieldInput = ({ }; const handleClickOutside = ( - _event: MouseEvent | TouchEvent, + event: MouseEvent | TouchEvent, newDate: Nullable, ) => { - onClickOutside?.(() => persistDate(newDate)); + onClickOutside?.(() => persistDate(newDate), event); }; const handleChange = (newDate: Nullable) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx index d933aeabc..661c63057 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx @@ -8,9 +8,13 @@ import { MultiItemFieldInput } from './MultiItemFieldInput'; type EmailsFieldInputProps = { onCancel?: () => void; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; -export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { +export const EmailsFieldInput = ({ + onCancel, + onClickOutside, +}: EmailsFieldInputProps) => { const { persistEmailsField, hotkeyScope, fieldValue } = useEmailsField(); const emails = useMemo( @@ -45,6 +49,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { items={emails} onPersist={handlePersistEmails} onCancel={onCancel} + onClickOutside={onClickOutside} placeholder="Email" fieldMetadataType={FieldMetadataType.Emails} validateInput={validateInput} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx index c8d7a5f52..bdba34f0c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx @@ -4,7 +4,10 @@ import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput'; import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay'; import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty'; -import { FieldInputEvent } from './DateTimeFieldInput'; +import { + FieldInputClickOutsideEvent, + FieldInputEvent, +} from './DateTimeFieldInput'; const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = 'F‌‌irst name'; @@ -13,7 +16,7 @@ const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = 'L‌‌ast name'; type FullNameFieldInputProps = { - onClickOutside?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onTab?: FieldInputEvent; @@ -57,8 +60,9 @@ export const FullNameFieldInput = ({ event: MouseEvent | TouchEvent, newDoubleText: FieldDoubleText, ) => { - onClickOutside?.(() => - persistFullNameField(convertToFullName(newDoubleText)), + onClickOutside?.( + () => persistFullNameField(convertToFullName(newDoubleText)), + event, ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index e52cc95c0..055ee5fba 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -8,9 +8,13 @@ import { MultiItemFieldInput } from './MultiItemFieldInput'; type LinksFieldInputProps = { onCancel?: () => void; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; -export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => { +export const LinksFieldInput = ({ + onCancel, + onClickOutside, +}: LinksFieldInputProps) => { const { persistLinksField, hotkeyScope, fieldValue } = useLinksField(); const links = useMemo<{ url: string; label: string }[]>( @@ -49,6 +53,7 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => { items={links} onPersist={handlePersistLinks} onCancel={onCancel} + onClickOutside={onClickOutside} placeholder="URL" fieldMetadataType={FieldMetadataType.Links} validateInput={(input) => ({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index f33537852..0e8f07a84 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -41,6 +41,7 @@ type MultiItemFieldInputProps = { newItemLabel?: string; fieldMetadataType: FieldMetadataType; renderInput?: DropdownMenuInputProps['renderInput']; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; // Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ... @@ -57,24 +58,19 @@ export const MultiItemFieldInput = ({ newItemLabel, fieldMetadataType, renderInput, + onClickOutside, }: MultiItemFieldInputProps) => { const containerRef = useRef(null); const handleDropdownClose = () => { onCancel?.(); }; - const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => { - event.stopImmediatePropagation(); - if (inputValue?.trim().length > 0) { - handleSubmitInput(); - } else { - onCancel?.(); - } - }; - useListenClickOutside({ refs: [containerRef], - callback: handleDropdownCloseOutside, + callback: (event) => { + onClickOutside?.(event); + }, + listenerId: hotkeyScope, }); useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx index 21540d3c3..fe2abdf9e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem.tsx @@ -1,7 +1,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { IconBookmark, IconBookmarkPlus, @@ -37,23 +37,29 @@ export const MultiItemFieldMenuItem = ({ const handleMouseEnter = () => setIsHovered(true); const handleMouseLeave = () => { setIsHovered(false); - if (isDropdownOpen) { - closeDropdown(); - } }; - const handleDeleteClick = () => { + const handleDeleteClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + closeDropdown(); setIsHovered(false); onDelete?.(); }; - const handleSetAsPrimaryClick = () => { + const handleSetAsPrimaryClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + closeDropdown(); onSetAsPrimary?.(); }; - const handleEditClick = () => { + const handleEditClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + closeDropdown(); onEdit?.(); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index acebdcda9..b76fecda5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -88,6 +88,7 @@ export const MultiSelectFieldInput = ({ } resetSelectedItem(); }, + listenerId: 'MultiSelectFieldInput', }); const optionIds = filteredOptionsInDropDown.map((option) => option.value); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx index d5fead466..ad727b9f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/NumberFieldInput.tsx @@ -2,11 +2,12 @@ import { TextInput } from '@/ui/field/input/components/TextInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; import { useNumberField } from '../../hooks/useNumberField'; +import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; export type FieldInputEvent = (persist: () => void) => void; export type NumberFieldInputProps = { - onClickOutside?: FieldInputEvent; + onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; onEscape?: FieldInputEvent; onTab?: FieldInputEvent; @@ -40,7 +41,7 @@ export const NumberFieldInput = ({ event: MouseEvent | TouchEvent, newText: string, ) => { - onClickOutside?.(() => persistNumberField(newText)); + onClickOutside?.(() => persistNumberField(newText), event); }; const handleTab = (newText: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx index 07727b1ec..108d70f4d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx @@ -49,10 +49,14 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` type PhonesFieldInputProps = { onCancel?: () => void; + onClickOutside?: (event: MouseEvent | TouchEvent) => void; }; -export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => { - const { persistPhonesField, hotkeyScope, fieldDefinition, fieldValue } = +export const PhonesFieldInput = ({ + onCancel, + onClickOutside, +}: PhonesFieldInputProps) => { + const { persistPhonesField, hotkeyScope, fieldValue, fieldDefinition } = usePhonesField(); const phones = createPhonesFromFieldValue(fieldValue); @@ -83,6 +87,7 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => { { - onClickOutside?.(() => persistJsonField(newText)); + onClickOutside?.(() => persistJsonField(newText), event); }; const handleTab = (newText: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx index 4f62ed895..bd30e957d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx @@ -21,8 +21,6 @@ export const SelectFieldInput = ({ }: SelectFieldInputProps) => { const { persistField, fieldDefinition, fieldValue, hotkeyScope } = useSelectField(); - const [selectWrapperRef, setSelectWrapperRef] = - useState(null); const [filteredOptions, setFilteredOptions] = useState([]); @@ -62,7 +60,7 @@ export const SelectFieldInput = ({ ]; return ( -
+
{ - onClickOutside?.(() => persistField(newText.trim())); + onClickOutside?.(() => persistField(newText.trim()), event); }; const handleTab = (newText: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index 8d01e3d98..550a3aa9d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -1,12 +1,5 @@ import { Decorator, Meta, StoryObj } from '@storybook/react'; -import { - expect, - fireEvent, - fn, - userEvent, - waitFor, - within, -} from '@storybook/test'; +import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; import { useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; @@ -26,6 +19,7 @@ import { import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext'; +import { getCanvasElementForDropdownTesting } from 'twenty-ui'; import { RelationToOneFieldInput, RelationToOneFieldInputProps, @@ -136,8 +130,8 @@ export const Default: Story = { export const Submit: Story = { decorators: [ComponentWithRecoilScopeDecorator], - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); + play: async () => { + const canvas = within(getCanvasElementForDropdownTesting()); expect(submitJestFn).toHaveBeenCalledTimes(0); @@ -154,16 +148,16 @@ export const Submit: Story = { export const Cancel: Story = { decorators: [ComponentWithRecoilScopeDecorator], - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); + play: async () => { + const canvas = within(getCanvasElementForDropdownTesting()); expect(cancelJestFn).toHaveBeenCalledTimes(0); await canvas.findByText('John Wick', undefined, { timeout: 3000 }); const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); - fireEvent.click(emptyDiv); await waitFor(() => { + userEvent.click(emptyDiv); expect(cancelJestFn).toHaveBeenCalledTimes(1); }); }, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts index 8e41832d2..519d5d77c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents.ts @@ -28,10 +28,10 @@ export const useRegisterInputEvents = ({ useListenClickOutside({ refs: [inputRef, copyRef].filter(isDefined), callback: (event) => { - event.stopImmediatePropagation(); onClickOutside?.(event, inputValue); }, enabled: isDefined(onClickOutside), + listenerId: hotkeyScope, }); useScopedHotkeys( diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index 3ba58d994..a8f137771 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -14,7 +14,11 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types import { useInlineCell } from '../hooks/useInlineCell'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; +import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; +import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; +import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; +import { useRecoilCallback } from 'recoil'; import { RecordInlineCellContainer } from './RecordInlineCellContainer'; import { RecordInlineCellContext, @@ -65,10 +69,30 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => { closeInlineCell(); }; - const handleClickOutside: FieldInputEvent = (persistField) => { - persistField(); - closeInlineCell(); - }; + const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback( + ({ snapshot }) => + (persistField, event) => { + const recordFieldDropdownId = getDropdownFocusIdForRecordField( + recordId, + fieldDefinition.fieldMetadataId, + 'inline-cell', + ); + + const activeDropdownFocusId = snapshot + .getLoadable(activeDropdownFocusIdState) + .getValue(); + + if (recordFieldDropdownId !== activeDropdownFocusId) { + return; + } + + event.stopImmediatePropagation(); + + persistField(); + closeInlineCell(); + }, + [closeInlineCell, fieldDefinition.fieldMetadataId, recordId], + ); const { getIcon } = useIcons(); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx index b2d187cc5..ac87c5341 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx @@ -18,11 +18,10 @@ const StyledInlineCellInput = styled.div` display: flex; min-height: 32px; - min-width: 240px; - width: inherit; + width: 240px; - z-index: 1000; + z-index: 30; `; type RecordInlineCellEditModeProps = { diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts index c9fc4c95f..4a4600d67 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts @@ -7,6 +7,9 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { isDefined } from '~/utils/isDefined'; import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2'; +import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; +import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState'; import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope'; @@ -21,6 +24,11 @@ export const useInlineCell = () => { isInlineCellInEditModeScopedState(recoilScopeId), ); + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + const { goBackToPreviousDropdownFocusId } = + useGoBackToPreviousDropdownFocusId(); + const { setHotkeyScopeAndMemorizePreviousScope, goBackToPreviousHotkeyScope, @@ -32,6 +40,8 @@ export const useInlineCell = () => { setIsInlineCellInEditMode(false); goBackToPreviousHotkeyScope(); + + goBackToPreviousDropdownFocusId(); }; const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => { @@ -46,6 +56,14 @@ export const useInlineCell = () => { } else { setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell); } + + setActiveDropdownFocusIdAndMemorizePrevious( + getDropdownFocusIdForRecordField( + recordId, + fieldDefinition.fieldMetadataId, + 'inline-cell', + ), + ); }; return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts index e60a16a12..33f97d7a1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts @@ -2,6 +2,7 @@ import { useRecoilCallback } from 'recoil'; import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState'; import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; +import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -16,6 +17,9 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { recordTableId, ); + const { goBackToPreviousDropdownFocusId } = + useGoBackToPreviousDropdownFocusId(); + return useRecoilCallback( ({ set, snapshot }) => { return async () => { @@ -28,8 +32,14 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { isTableCellInEditModeFamilyState(currentTableCellInEditModePosition), false, ); + + goBackToPreviousDropdownFocusId(); }; }, - [currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState], + [ + currentTableCellInEditModePositionState, + isTableCellInEditModeFamilyState, + goBackToPreviousDropdownFocusId, + ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx index e8fab2be7..df9bfbc15 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx @@ -5,7 +5,7 @@ import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; type RecordTableBodyUnselectEffectProps = { tableBodyRef: React.RefObject; @@ -32,7 +32,7 @@ export const RecordTableBodyUnselectEffect = ({ TableHotkeyScope.Table, ); - useListenClickOutsideV2({ + useListenClickOutside({ excludeClassNames: [ 'bottom-bar', 'action-menu-dropdown', diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx index 8024f314a..beba813cc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx @@ -4,19 +4,13 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; -import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; -import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates'; -import { useSetRecoilState } from 'recoil'; +import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; +import { useRecoilCallback } from 'recoil'; export const RecordTableCellFieldInput = () => { - const { getClickOutsideListenerIsActivatedState } = - useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID); - const setClickOutsideListenerIsActivated = useSetRecoilState( - getClickOutsideListenerIsActivatedState, - ); - const { onUpsertRecord, onMoveFocus, onCloseTableCell } = useContext(RecordTableContext); @@ -48,17 +42,35 @@ export const RecordTableCellFieldInput = () => { onCloseTableCell(); }; - const handleClickOutside: FieldInputEvent = (persistField) => { - setClickOutsideListenerIsActivated(false); + const handleClickOutside = useRecoilCallback( + ({ snapshot }) => + (persistField: () => void, event: MouseEvent | TouchEvent) => { + const dropdownFocusId = getDropdownFocusIdForRecordField( + recordId, + fieldDefinition.fieldMetadataId, + 'table-cell', + ); - onUpsertRecord({ - persistField, - recordId, - fieldName: fieldDefinition.metadata.fieldName, - }); + const activeDropdownFocusId = snapshot + .getLoadable(activeDropdownFocusIdState) + .getValue(); - onCloseTableCell(); - }; + if (activeDropdownFocusId !== dropdownFocusId) { + return; + } + + event.stopImmediatePropagation(); + + onUpsertRecord({ + persistField, + recordId, + fieldName: fieldDefinition.metadata.fieldName, + }); + + onCloseTableCell(); + }, + [fieldDefinition, onCloseTableCell, onUpsertRecord, recordId], + ); const handleEscape: FieldInputEvent = (persistField) => { onUpsertRecord({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index d9c47b826..b2210fce3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -10,13 +10,11 @@ import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEm import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode'; import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton'; import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell'; import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from '~/utils/isDefined'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; @@ -34,7 +32,6 @@ export const RecordTableCellSoftFocusMode = ({ nonEditModeContent, }: RecordTableCellSoftFocusModeProps) => { const { columnIndex } = useContext(RecordTableCellContext); - const closeCurrentTableCell = useCloseCurrentTableCellInEditMode(); const isFieldReadOnly = useIsFieldValueReadOnly(); @@ -135,13 +132,6 @@ export const RecordTableCellSoftFocusMode = ({ */ }; - useListenClickOutside({ - refs: [scrollRef], - callback: () => { - closeCurrentTableCell(); - }, - }); - const isFirstColumn = columnIndex === 0; const customButtonIcon = useGetButtonIcon(); const buttonIcon = isFirstColumn diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts index debeb9a4f..7e885b4ad 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts @@ -22,6 +22,8 @@ import { isDefined } from '~/utils/isDefined'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; +import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates'; import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -69,6 +71,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { const navigate = useNavigate(); + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + const openTableCell = useRecoilCallback( ({ snapshot, set }) => ({ @@ -142,6 +147,14 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { DEFAULT_CELL_SCOPE.customScopes, ); } + + setActiveDropdownFocusIdAndMemorizePrevious( + getDropdownFocusIdForRecordField( + recordId, + fieldDefinition.fieldMetadataId, + 'table-cell', + ), + ); }, [ getClickOutsideListenerIsActivatedState, @@ -156,6 +169,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { setViewableRecordNameSingular, openRightDrawer, setHotkeyScope, + setActiveDropdownFocusIdAndMemorizePrevious, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 31bcc917f..124d9a388 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -15,7 +15,6 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState'; import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useTableColumns } from '../../hooks/useTableColumns'; import { ColumnDefinition } from '../../types/ColumnDefinition'; @@ -92,45 +91,43 @@ export const RecordTableColumnHeadDropdownMenu = ({ const canHide = column.isLabelIdentifier !== true; return ( - - - {isFilterable && ( - - )} - {isSortable && ( - - )} - {showSeparator && } - {canMoveLeft && ( - - )} - {canMoveRight && ( - - )} - {canHide && ( - - )} - - + + {isFilterable && ( + + )} + {isSortable && ( + + )} + {showSeparator && } + {canMoveLeft && ( + + )} + {canMoveRight && ( + + )} + {canHide && ( + + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index ff78e7c3f..fa7f44137 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -13,7 +13,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableHeaderPlusButtonContent = () => { @@ -42,20 +41,18 @@ export const RecordTableHeaderPlusButtonContent = () => { return ( <> - - - {hiddenTableColumns.map((column) => ( - handleAddColumn(column)} - LeftIcon={getIcon(column.iconName)} - text={column.label} - /> - ))} - - - + {hiddenTableColumns.map((column) => ( + handleAddColumn(column)} + LeftIcon={getIcon(column.iconName)} + text={column.label} + /> + ))} + + + 0 && ( )} - {isDefined(onCreate) &&
{createNewButton}
} + {isDefined(onCreate) && ( + + {createNewButton} + + )} )} diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx index a3cb699c7..2fe72f8c7 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx @@ -3,6 +3,7 @@ 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'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; export const MultipleObjectRecordOnClickOutsideEffect = ({ containerRef, @@ -11,10 +12,6 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({ containerRef: React.RefObject; onClickOutside: () => void; }) => { - const { useListenClickOutside } = useClickOutsideListener( - MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID, - ); - const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } = useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID); @@ -35,6 +32,7 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({ onClickOutside(); }, + listenerId: MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID, }); return <>; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelect.tsx index 221fa3961..839938183 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelect.tsx @@ -41,6 +41,7 @@ export const SingleRecordSelect = ({ onCancel(); } }, + listenerId: 'single-record-select', }); return ( diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItemsWithSearch.tsx index 4eb6ac108..3952c8852 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItemsWithSearch.tsx @@ -59,27 +59,6 @@ export const SingleRecordSelectMenuItemsWithSearch = ({ /> ); - const results = ( - - ); - return ( <> {dropdownPlacement?.includes('end') && ( @@ -88,7 +67,26 @@ export const SingleRecordSelectMenuItemsWithSearch = ({ {createNewButton}
{records.recordsToSelect.length > 0 && } - {records.recordsToSelect.length > 0 && results} + {records.recordsToSelect.length > 0 && ( + + )} )} @@ -97,7 +95,26 @@ export const SingleRecordSelectMenuItemsWithSearch = ({ isUndefinedOrNull(dropdownPlacement)) && ( <> - {records.recordsToSelect.length > 0 && results} + {records.recordsToSelect.length > 0 && ( + + )} {records.recordsToSelect.length > 0 && isDefined(onCreate) && ( )} diff --git a/packages/twenty-front/src/modules/object-record/utils/getDropdownFocusIdForRecordField.ts b/packages/twenty-front/src/modules/object-record/utils/getDropdownFocusIdForRecordField.ts new file mode 100644 index 000000000..b5298f9d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getDropdownFocusIdForRecordField.ts @@ -0,0 +1,7 @@ +export const getDropdownFocusIdForRecordField = ( + recordId: string, + fieldMetadataId: string, + componentType: 'table-cell' | 'inline-cell', +) => { + return `dropdown-${componentType}-record-${recordId}-field-${fieldMetadataId}`; +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx index b03bb533d..55bdd9393 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx @@ -19,12 +19,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; type SettingsAccountsRowDropdownMenuProps = { account: ConnectedAccount; - className?: string; }; export const SettingsAccountsRowDropdownMenu = ({ account, - className, }: SettingsAccountsRowDropdownMenuProps) => { const dropdownId = `settings-account-row-${account.id}`; @@ -39,7 +37,6 @@ export const SettingsAccountsRowDropdownMenu = ({ return ( { setIsOpen(false); }, + listenerId: 'match-column-select', }); useUpdateEffect(() => { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx index 27aa269d8..cdce6b1e1 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx @@ -75,8 +75,6 @@ export const SubMatchingSelect = ({ const options = getFieldOptions(fields, column.value) as SelectOption[]; const value = options.find((opt) => opt.value === option.value); const [isOpen, setIsOpen] = useState(false); - const [selectWrapperRef, setSelectWrapperRef] = - useState(null); const theme = useTheme(); @@ -106,17 +104,14 @@ export const SubMatchingSelect = ({ cursor="pointer" onClick={() => setIsOpen(!isOpen)} id="control" - ref={setSelectWrapperRef} > - {isOpen && ( theme.background.secondary}; @@ -190,18 +191,22 @@ export const AddressInput = ({ [onEscape, internalValue], ); - const { useListenClickOutside } = useClickOutsideListener('addressInput'); + const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState); useListenClickOutside({ refs: [wrapperRef], callback: (event) => { + if (activeDropdownFocusId === SELECT_COUNTRY_DROPDOWN_ID) { + return; + } + event.stopImmediatePropagation(); closeCountryDropdown(); - onClickOutside?.(event, internalValue); }, enabled: isDefined(onClickOutside), + listenerId: 'address-input', }); useEffect(() => { diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx index c19c165d2..858436a97 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx @@ -9,7 +9,7 @@ import { MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; const StyledCalendarContainer = styled.div` background: ${({ theme }) => theme.background.transparent.secondary}; @@ -72,7 +72,7 @@ export const DateInput = ({ MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, ); - useListenClickOutsideV2({ + useListenClickOutside({ refs: [wrapperRef], listenerId: 'DateInput', callback: (event) => { diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx index c003bde59..b661e215f 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx @@ -10,9 +10,9 @@ import { Key } from 'ts-key-enum'; import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from '~/utils/isDefined'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { splitFullName } from '~/utils/format/spiltFullName'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; import { StyledTextInput } from './TextInput'; @@ -158,6 +158,7 @@ export const DoubleTextInput = ({ }); }, enabled: isDefined(onClickOutside), + listenerId: 'double-text-input', }); const handleOnPaste = (event: ClipboardEvent) => { diff --git a/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx b/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx index 6be66c368..f7fafb89c 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/FieldTextAreaOverlay.tsx @@ -2,13 +2,13 @@ import styled from '@emotion/styled'; import { OVERLAY_BACKGROUND } from 'twenty-ui'; const StyledFieldTextAreaOverlay = styled.div` + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.sm}; + display: flex; + margin: -1px; + max-height: 420px; position: absolute; top: 0; - border-radius: ${({ theme }) => theme.border.radius.sm}; - align-items: center; - display: flex; - max-height: 420px; - margin: -1px; width: 100%; ${OVERLAY_BACKGROUND} `; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx index 56d62331b..73749a608 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/SelectInput.tsx @@ -1,14 +1,12 @@ import { SelectOption } from '@/spreadsheet-import/types'; import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { ReferenceType } from '@floating-ui/react'; type SelectInputProps = { selectableListId: string; selectableItemIdArray: string[]; hotkeyScope: string; onEnter: (itemId: string) => void; - selectWrapperRef?: ReferenceType | null | undefined; onOptionSelected: (selectedOption: SelectOption) => void; options: SelectOption[]; onCancel?: () => void; @@ -23,7 +21,6 @@ export const SelectInput = ({ selectableItemIdArray, hotkeyScope, onEnter, - selectWrapperRef, onOptionSelected, options, onCancel, @@ -40,7 +37,6 @@ export const SelectInput = ({ onEnter={onEnter} > ({ )} {!!callToActionButton && ( - + theme.lastLayerZIndex}; -`; - interface SelectInputProps { onOptionSelected: (selectedOption: SelectOption) => void; options: SelectOption[]; onCancel?: () => void; defaultOption?: SelectOption; - parentRef?: ReferenceType | null | undefined; onFilterChange?: (filteredOptions: SelectOption[]) => void; onClear?: () => void; clearLabel?: string; @@ -47,13 +28,11 @@ export const SelectInput = ({ options, onCancel, defaultOption, - parentRef, onFilterChange, hotkeyScope, }: SelectInputProps) => { const containerRef = useRef(null); - const theme = useTheme(); const [searchFilter, setSearchFilter] = useState(''); const [selectedOption, setSelectedOption] = useState< SelectOption | undefined @@ -81,27 +60,12 @@ export const SelectInput = ({ onOptionSelected(option); }; - const { refs, floatingStyles } = useFloating({ - elements: { reference: parentRef }, - strategy: 'absolute', - middleware: [ - offset(() => { - return parseInt(theme.spacing(2), 10); - }), - flip(), - size(), - ], - whileElementsMounted: autoUpdate, - open: true, - placement: 'bottom-start', - }); - useEffect(() => { onFilterChange?.(optionsInDropDown); }, [onFilterChange, optionsInDropDown]); useListenClickOutside({ - refs: [refs.floating], + refs: [containerRef], callback: (event) => { event.stopImmediatePropagation(); @@ -113,6 +77,7 @@ export const SelectInput = ({ onCancel(); } }, + listenerId: 'select-input', }); useScopedHotkeys( @@ -130,44 +95,39 @@ export const SelectInput = ({ ); return ( - - - setSearchFilter(e.target.value)} - autoFocus - /> - - - {onClear && clearLabel && ( + + setSearchFilter(e.target.value)} + autoFocus + /> + + + {onClear && clearLabel && ( + { + setSelectedOption(undefined); + onClear(); + }} + /> + )} + {optionsInDropDown.map((option) => { + return ( { - setSelectedOption(undefined); - onClear(); - }} + key={option.value} + selected={selectedOption?.value === option.value} + text={option.label} + color={option.color as TagColor} + onClick={() => handleOptionChange(option)} /> - )} - {optionsInDropDown.map((option) => { - return ( - handleOptionChange(option)} - /> - ); - })} - - - + ); + })} + + ); }; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx index 555c0b273..f46e546be 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton.tsx @@ -10,11 +10,7 @@ import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope'; import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect'; -type StyledDropdownButtonProps = { - isUnfolded: boolean; -}; - -const StyledDropdownButtonContainer = styled.div` +const StyledDropdownButtonContainer = styled.div` align-items: center; color: ${({ color }) => color ?? 'none'}; cursor: pointer; @@ -62,7 +58,7 @@ export const CurrencyPickerDropdownButton = ({ }) => { const theme = useTheme(); - const { isDropdownOpen, closeDropdown } = useDropdown( + const { closeDropdown } = useDropdown( CurrencyPickerHotkeyScope.CurrencyPicker, ); @@ -77,11 +73,10 @@ export const CurrencyPickerDropdownButton = ({ return ( + {currencyCode} diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx index 0762391bf..e94c81808 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/currency/components/CurrencyPickerDropdownSelect.tsx @@ -32,7 +32,7 @@ export const CurrencyPickerDropdownSelect = ({ ); return ( - + setSearchFilter(event.target.value)} diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 05bca34bc..2c45e4fba 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -1,32 +1,26 @@ import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; -import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { autoUpdate, flip, - FloatingPortal, offset, Placement, size, useFloating, } from '@floating-ui/react'; -import { MouseEvent, ReactNode, useEffect, useRef } from 'react'; -import { flushSync } from 'react-dom'; +import { MouseEvent, ReactNode } from 'react'; import { Keys } from 'react-hotkeys-hook'; -import { Key } from 'ts-key-enum'; -import { isDefined } from '~/utils/isDefined'; import { useDropdown } from '../hooks/useDropdown'; -import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownUnmountEffect } from '@/ui/layout/dropdown/components/DropdownUnmountEffect'; import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext'; import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2'; -import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; -import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { DropdownMenu } from './DropdownMenu'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { flushSync } from 'react-dom'; +import { isDefined } from 'twenty-ui'; import { DropdownOnToggleEffect } from './DropdownOnToggleEffect'; type DropdownProps = { @@ -62,24 +56,15 @@ export const Dropdown = ({ dropdownStrategy = 'absolute', dropdownOffset = { x: 0, y: 0 }, disableBlur = false, - usePortal = false, onClickOutside, onClose, onOpen, }: DropdownProps) => { - const containerRef = useRef(null); - - const { - isDropdownOpen, - toggleDropdown, - closeDropdown, - dropdownWidth, - setDropdownPlacement, - } = useDropdown(dropdownId); + const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId); const offsetMiddlewares = []; - const [dropdownMaxHeight, setDropdownMaxHeight] = useRecoilComponentStateV2( + const setDropdownMaxHeight = useSetRecoilComponentStateV2( dropdownMaxHeightComponentStateV2, dropdownId, ); @@ -111,14 +96,6 @@ export const Dropdown = ({ strategy: dropdownStrategy, }); - useEffect(() => { - setDropdownPlacement(placement); - }, [placement, setDropdownPlacement]); - - const handleHotkeyTriggered = () => { - toggleDropdown(); - }; - const handleClickableComponentClick = (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); @@ -127,88 +104,41 @@ export const Dropdown = ({ onClickOutside?.(); }; - useListenClickOutsideV2({ - refs: [refs.floating, refs.domReference], - listenerId: dropdownId, - callback: () => { - onClickOutside?.(); - if (isDropdownOpen) { - closeDropdown(); - } - }, - }); - - useInternalHotkeyScopeManagement({ - dropdownScopeId: getScopeIdFromComponentId(dropdownId), - dropdownHotkeyScopeFromParent: dropdownHotkeyScope, - }); - - useScopedHotkeys( - [Key.Escape], - () => { - if (isDropdownOpen) { - closeDropdown(); - } - }, - dropdownHotkeyScope.scope, - [closeDropdown, isDropdownOpen], - ); - - const dropdownMenuStyles = { - ...floatingStyles, - maxHeight: dropdownMaxHeight, - }; - return ( -
+ <> {clickableComponent && (
{clickableComponent}
)} - {hotkey && ( - - )} - {isDropdownOpen && usePortal && ( - - - {dropdownComponents} - - - )} - {isDropdownOpen && !usePortal && ( - - {dropdownComponents} - + dropdownMenuWidth={dropdownMenuWidth} + dropdownComponents={dropdownComponents} + dropdownId={dropdownId} + dropdownPlacement={placement ?? 'bottom-end'} + floatingUiRefs={refs} + hotkeyScope={dropdownHotkeyScope} + hotkey={hotkey} + onClickOutside={onClickOutside} + onHotkeyTriggered={toggleDropdown} + /> )} -
+
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownContent.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownContent.tsx new file mode 100644 index 000000000..113338240 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownContent.tsx @@ -0,0 +1,128 @@ +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement'; +import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; +import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2'; +import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { + FloatingPortal, + Placement, + UseFloatingReturn, +} from '@floating-ui/react'; +import { useEffect } from 'react'; +import { Keys } from 'react-hotkeys-hook'; +import { useRecoilValue } from 'recoil'; +import { Key } from 'ts-key-enum'; + +export type DropdownContentProps = { + className?: string; + dropdownId: string; + dropdownPlacement: Placement; + floatingUiRefs: UseFloatingReturn['refs']; + onClickOutside?: () => void; + hotkeyScope: HotkeyScope; + floatingStyles: UseFloatingReturn['floatingStyles']; + hotkey?: { + key: Keys; + scope: string; + }; + onHotkeyTriggered?: () => void; + disableBlur?: boolean; + dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number; + dropdownComponents: React.ReactNode; + parentDropdownId?: string; +}; + +export const DropdownContent = ({ + className, + dropdownId, + dropdownPlacement, + floatingUiRefs, + onClickOutside, + hotkeyScope, + floatingStyles, + hotkey, + onHotkeyTriggered, + disableBlur, + dropdownMenuWidth, + dropdownComponents, +}: DropdownContentProps) => { + const { isDropdownOpen, closeDropdown, dropdownWidth, setDropdownPlacement } = + useDropdown(dropdownId); + + const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState); + + const [dropdownMaxHeight] = useRecoilComponentStateV2( + dropdownMaxHeightComponentStateV2, + dropdownId, + ); + + useEffect(() => { + setDropdownPlacement(dropdownPlacement); + }, [dropdownPlacement, setDropdownPlacement]); + + useListenClickOutside({ + refs: [floatingUiRefs.floating, floatingUiRefs.domReference], + listenerId: dropdownId, + callback: (event) => { + if (activeDropdownFocusId !== dropdownId) return; + + if (isDropdownOpen) { + event.stopImmediatePropagation(); + event.preventDefault(); + + closeDropdown(); + } + + onClickOutside?.(); + }, + }); + + useInternalHotkeyScopeManagement({ + dropdownScopeId: getScopeIdFromComponentId(dropdownId), + dropdownHotkeyScopeFromParent: hotkeyScope, + }); + + useScopedHotkeys( + [Key.Escape], + () => { + if (activeDropdownFocusId !== dropdownId) return; + + if (isDropdownOpen) { + closeDropdown(); + } + }, + hotkeyScope?.scope, + [closeDropdown, isDropdownOpen], + ); + + const dropdownMenuStyles = { + ...floatingStyles, + maxHeight: dropdownMaxHeight, + }; + + return ( + <> + {hotkey && onHotkeyTriggered && ( + + )} + + + {dropdownComponents} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx index 911d8d520..02ed2027b 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx @@ -18,7 +18,6 @@ const StyledHeader = styled.li` padding: ${({ theme }) => theme.spacing(1)}; user-select: none; - width: inherit; &:hover { background: ${({ theme, onClick }) => diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx index f59a0aa3a..6f3e9d83f 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx @@ -44,7 +44,7 @@ const StyledInputContainer = styled.div` const StyledRightContainer = styled.div` position: absolute; - right: ${({ theme }) => theme.spacing(1)}; + right: ${({ theme }) => theme.spacing(2)}; top: 50%; transform: translateY(-50%); `; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx index 1a4a197c7..78244082f 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx @@ -37,12 +37,14 @@ export const DropdownMenuItemsContainer = ({ children, hasMaxHeight, className, + withoutScrollWrapper, }: { children: React.ReactNode; hasMaxHeight?: boolean; className?: string; + withoutScrollWrapper?: boolean; }) => { - return ( + return withoutScrollWrapper === true ? ( )} + ) : ( + + + + {children} + + + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts index 24225beab..9de962653 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts @@ -1,6 +1,8 @@ import { useRecoilState } from 'recoil'; import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates'; +import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; import { useCallback } from 'react'; @@ -17,6 +19,12 @@ export const useDropdown = (dropdownId?: string) => { dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId), }); + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + + const { goBackToPreviousDropdownFocusId } = + useGoBackToPreviousDropdownFocusId(); + const { setHotkeyScopeAndMemorizePreviousScope, goBackToPreviousHotkeyScope, @@ -34,17 +42,28 @@ export const useDropdown = (dropdownId?: string) => { useRecoilState(isDropdownOpenState); const closeDropdown = useCallback(() => { - goBackToPreviousHotkeyScope(); - setIsDropdownOpen(false); - }, [goBackToPreviousHotkeyScope, setIsDropdownOpen]); + if (isDropdownOpen) { + goBackToPreviousHotkeyScope(); + setIsDropdownOpen(false); + goBackToPreviousDropdownFocusId(); + } + }, [ + isDropdownOpen, + goBackToPreviousHotkeyScope, + setIsDropdownOpen, + goBackToPreviousDropdownFocusId, + ]); const openDropdown = () => { - setIsDropdownOpen(true); - if (isDefined(dropdownHotkeyScope)) { - setHotkeyScopeAndMemorizePreviousScope( - dropdownHotkeyScope.scope, - dropdownHotkeyScope.customScopes, - ); + if (!isDropdownOpen) { + setIsDropdownOpen(true); + setActiveDropdownFocusIdAndMemorizePrevious(dropdownId ?? scopeId); + if (isDefined(dropdownHotkeyScope)) { + setHotkeyScopeAndMemorizePreviousScope( + dropdownHotkeyScope.scope, + dropdownHotkeyScope.customScopes, + ); + } } }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts new file mode 100644 index 000000000..5c336ac6c --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts @@ -0,0 +1,23 @@ +import { useRecoilCallback } from 'recoil'; + +import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; +import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState'; + +export const useGoBackToPreviousDropdownFocusId = () => { + const goBackToPreviousDropdownFocusId = useRecoilCallback( + ({ snapshot, set }) => + () => { + const previouslyFocusedDropdownId = snapshot + .getLoadable(previousDropdownFocusIdState) + .getValue(); + + set(activeDropdownFocusIdState, previouslyFocusedDropdownId); + set(previousDropdownFocusIdState, null); + }, + [], + ); + + return { + goBackToPreviousDropdownFocusId, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts new file mode 100644 index 000000000..ad7bd82db --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts @@ -0,0 +1,23 @@ +import { useRecoilCallback } from 'recoil'; + +import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; +import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState'; + +export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => { + const setActiveDropdownFocusIdAndMemorizePrevious = useRecoilCallback( + ({ snapshot, set }) => + (dropdownId: string) => { + const focusedDropdownId = snapshot + .getLoadable(activeDropdownFocusIdState) + .getValue(); + + set(previousDropdownFocusIdState, focusedDropdownId); + set(activeDropdownFocusIdState, dropdownId); + }, + [], + ); + + return { + setActiveDropdownFocusIdAndMemorizePrevious, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/activeDropdownFocusIdState.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/activeDropdownFocusIdState.ts new file mode 100644 index 000000000..fca8a5ba6 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/activeDropdownFocusIdState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const activeDropdownFocusIdState = createState({ + key: 'activeDropdownFocusIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts new file mode 100644 index 000000000..e311d1f3f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const previousDropdownFocusIdState = createState({ + key: 'previousDropdownFocusIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx index db901cab9..b1d19104e 100644 --- a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx @@ -4,7 +4,6 @@ import { AnimatedContainer, Chip, ChipVariant } from 'twenty-ui'; import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown'; import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from '~/utils/isDefined'; const StyledContainer = styled.div` @@ -101,20 +100,18 @@ export const ExpandableList = ({ resetFirstHiddenChildIndex(); }, [isChipCountDisplayed, children.length, resetFirstHiddenChildIndex]); - useListenClickOutside({ - refs: [containerRef], - callback: () => { - // Handle container resize - if ( - childrenContainerElement?.clientWidth !== previousChildrenContainerWidth - ) { - resetFirstHiddenChildIndex(); - setPreviousChildrenContainerWidth( - childrenContainerElement?.clientWidth ?? 0, - ); - } - }, - }); + const handleClickOutside = () => { + setIsListExpanded(false); + + if ( + childrenContainerElement?.clientWidth !== previousChildrenContainerWidth + ) { + resetFirstHiddenChildIndex(); + setPreviousChildrenContainerWidth( + childrenContainerElement?.clientWidth ?? 0, + ); + } + }; return ( { - resetFirstHiddenChildIndex(); - setIsListExpanded(false); - }} + onClickOutside={handleClickOutside} withBorder={withExpandedListBorder} > {children} diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedListDropdown.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedListDropdown.tsx index 19afb7852..72d7d00d5 100644 --- a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedListDropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedListDropdown.tsx @@ -46,8 +46,11 @@ export const ExpandedListDropdown = ({ }); useListenClickOutside({ - refs: [refs.floating], - callback: onClickOutside ?? (() => {}), + refs: [refs.domReference], + callback: () => { + onClickOutside?.(); + }, + listenerId: 'expandable-list', }); return ( 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 fa5585207..1941c1d75 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 @@ -3,8 +3,8 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { ClickOutsideMode, - useListenClickOutsideV2, -} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; + useListenClickOutside, +} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; @@ -207,7 +207,7 @@ export const Modal = ({ hotkeyScope, ); - useListenClickOutsideV2({ + useListenClickOutside({ refs: [modalRef], listenerId: 'MODAL_CLICK_OUTSIDE_LISTENER_ID', callback: () => { diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index 437dd28c9..dc662aa44 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -1,26 +1,19 @@ -import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState'; import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; -import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -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 { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; -import { useRef } from 'react'; -import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; -import { Key } from 'ts-key-enum'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { isDefined } from '~/utils/isDefined'; -import { useRightDrawer } from '../hooks/useRightDrawer'; import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; -import { RightDrawerHotkeyScope } from '../types/RightDrawerHotkeyScope'; +import { RIGHT_DRAWER_ANIMATION_VARIANTS } from '@/ui/layout/right-drawer/constants/RightDrawerAnimationVariants'; +import { RightDrawerAnimationVariant } from '@/ui/layout/right-drawer/types/RightDrawerAnimationVariant'; import { RightDrawerRouter } from './RightDrawerRouter'; -import { workflowReactFlowRefState } from '@/workflow/states/workflowReactFlowRefState'; const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>` background: ${({ theme }) => theme.background.primary}; @@ -40,7 +33,7 @@ const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>` right: 0; top: 0; - z-index: 100; + z-index: 30; .modal-backdrop { background: ${({ theme }) => theme.background.overlayTertiary}; @@ -56,39 +49,6 @@ const StyledRightDrawer = styled.div` export const RightDrawer = () => { const theme = useTheme(); - const animationVariants = { - fullScreen: { - x: '0%', - width: '100%', - height: '100%', - bottom: '0', - top: '0', - }, - normal: { - x: '0%', - width: theme.rightDrawerWidth, - height: '100%', - bottom: '0', - top: '0', - }, - closed: { - x: '100%', - width: '0', - height: '100%', - bottom: '0', - top: 'auto', - }, - minimized: { - x: '0%', - width: 220, - height: 41, - bottom: '0', - top: 'auto', - }, - }; - - type RightDrawerAnimationVariant = keyof typeof animationVariants; - const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); @@ -99,52 +59,6 @@ export const RightDrawer = () => { const rightDrawerPage = useRecoilValue(rightDrawerPageState); - const { closeRightDrawer } = useRightDrawer(); - - const rightDrawerRef = useRef(null); - const workflowReactFlowRef = useRecoilValue(workflowReactFlowRefState); - - const { useListenClickOutside } = useClickOutsideListener( - RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID, - ); - - useListenClickOutside({ - refs: [ - rightDrawerRef, - ...(workflowReactFlowRef ? [workflowReactFlowRef] : []), - ], - callback: useRecoilCallback( - ({ snapshot, set }) => - (event) => { - const isRightDrawerOpen = snapshot - .getLoadable(isRightDrawerOpenState) - .getValue(); - const isRightDrawerMinimized = snapshot - .getLoadable(isRightDrawerMinimizedState) - .getValue(); - - if (isRightDrawerOpen && !isRightDrawerMinimized) { - set(rightDrawerCloseEventState, event); - - closeRightDrawer(); - } - }, - [closeRightDrawer], - ), - mode: ClickOutsideMode.comparePixels, - }); - - useScopedHotkeys( - [Key.Escape], - () => { - if (isRightDrawerOpen && !isRightDrawerMinimized) { - closeRightDrawer(); - } - }, - RightDrawerHotkeyScope.RightDrawer, - [isRightDrawerOpen, isRightDrawerMinimized], - ); - const isMobile = useIsMobile(); const targetVariantForAnimation: RightDrawerAnimationVariant = @@ -168,13 +82,13 @@ export const RightDrawer = () => { - + {isRightDrawerOpen && } diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerContainer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerContainer.tsx new file mode 100644 index 000000000..578d26830 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerContainer.tsx @@ -0,0 +1,83 @@ +import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; +import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; +import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { + ClickOutsideMode, + useListenClickOutside, +} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { workflowReactFlowRefState } from '@/workflow/states/workflowReactFlowRefState'; +import styled from '@emotion/styled'; +import { useRef } from 'react'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { Key } from 'ts-key-enum'; + +const StyledRightDrawerPage = styled.div` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +`; + +export const RightDrawerContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + const rightDrawerRef = useRef(null); + + const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); + + const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); + + const { closeRightDrawer } = useRightDrawer(); + + const workflowReactFlowRef = useRecoilValue(workflowReactFlowRefState); + + useListenClickOutside({ + refs: [ + rightDrawerRef, + ...(workflowReactFlowRef ? [workflowReactFlowRef] : []), + ], + listenerId: RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID, + callback: useRecoilCallback( + ({ snapshot, set }) => + (event) => { + const isRightDrawerOpen = snapshot + .getLoadable(isRightDrawerOpenState) + .getValue(); + const isRightDrawerMinimized = snapshot + .getLoadable(isRightDrawerMinimizedState) + .getValue(); + + if (isRightDrawerOpen && !isRightDrawerMinimized) { + set(rightDrawerCloseEventState, event); + + closeRightDrawer(); + } + }, + [closeRightDrawer], + ), + mode: ClickOutsideMode.comparePixels, + }); + + useScopedHotkeys( + [Key.Escape], + () => { + if (isRightDrawerOpen && !isRightDrawerMinimized) { + closeRightDrawer(); + } + }, + RightDrawerHotkeyScope.RightDrawer, + [isRightDrawerOpen, isRightDrawerMinimized], + ); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx index 820996bda..8bcb03882 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx @@ -7,22 +7,16 @@ import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/compone import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord'; import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; +import { RightDrawerContainer } from '@/ui/layout/right-drawer/components/RightDrawerContainer'; import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar'; import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage'; import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep'; import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction'; +import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/components/RightDrawerWorkflowSelectTriggerType'; import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep'; import { isDefined } from 'twenty-ui'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerPages } from '../types/RightDrawerPages'; -import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/components/RightDrawerWorkflowSelectTriggerType'; - -const StyledRightDrawerPage = styled.div` - display: flex; - flex-direction: column; - height: 100%; - width: 100%; -`; const StyledRightDrawerBody = styled.div` display: flex; @@ -61,13 +55,13 @@ export const RightDrawerRouter = () => { const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); return ( - + {!isRightDrawerMinimized && ( {rightDrawerPageComponent} )} - + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerAnimationVariants.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerAnimationVariants.ts new file mode 100644 index 000000000..710f38f68 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerAnimationVariants.ts @@ -0,0 +1,32 @@ +import { THEME_COMMON } from 'twenty-ui'; + +export const RIGHT_DRAWER_ANIMATION_VARIANTS = { + fullScreen: { + x: '0%', + width: '100%', + height: '100%', + bottom: '0', + top: '0', + }, + normal: { + x: '0%', + width: THEME_COMMON.rightDrawerWidth, + height: '100%', + bottom: '0', + top: '0', + }, + closed: { + x: '100%', + width: '0', + height: '100%', + bottom: '0', + top: 'auto', + }, + minimized: { + x: '0%', + width: 220, + height: 41, + bottom: '0', + top: 'auto', + }, +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerAnimationVariant.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerAnimationVariant.ts new file mode 100644 index 000000000..bbe135824 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerAnimationVariant.ts @@ -0,0 +1,4 @@ +import { RIGHT_DRAWER_ANIMATION_VARIANTS } from '@/ui/layout/right-drawer/constants/RightDrawerAnimationVariants'; + +export type RightDrawerAnimationVariant = + keyof typeof RIGHT_DRAWER_ANIMATION_VARIANTS; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx index 0e0cee09f..95b7b52e6 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerInput.tsx @@ -96,6 +96,7 @@ export const NavigationDrawerInput = ({ event.stopImmediatePropagation(); onClickOutside(event, value); }, + listenerId: 'navigation-drawer-input', }); const handleChange = (event: ChangeEvent) => { diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideV2.test.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx similarity index 92% rename from packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideV2.test.tsx rename to packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx index 1afd8d13a..9d0a26347 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideV2.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutside.test.tsx @@ -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(); @@ -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, diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx deleted file mode 100644 index 2f9530aeb..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx +++ /dev/null @@ -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(); -const nullRef = React.createRef(); - -const Wrapper = ({ children }: { children: React.ReactNode }) => ( -
{children}
-); - -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(); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts index 5a693e1f7..819c9a294 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts @@ -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 = ({ - callback, - refs, - enabled, - mode, - }: Omit, '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, }; 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 613590ed5..a57d6ef92 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 @@ -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 = ({ - refs, - callback, - mode = ClickOutsideMode.compareHTMLRef, - enabled = true, -}: { +export type ClickOutsideListenerProps = { refs: Array>; + excludeClassNames?: string[]; callback: (event: MouseEvent | TouchEvent) => void; mode?: ClickOutsideMode; + listenerId: string; + hotkeyScope?: string; enabled?: boolean; -}) => { - const [isMouseDownInside, setIsMouseDownInside] = useState(false); +}; + +export const useListenClickOutside = ({ + refs, + excludeClassNames, + callback, + mode = ClickOutsideMode.compareHTMLRef, + listenerId, + hotkeyScope, + enabled = true, +}: ClickOutsideListenerProps) => { + 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]); }; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts deleted file mode 100644 index ace1e9f34..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts +++ /dev/null @@ -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 = { - refs: Array>; - excludeClassNames?: string[]; - callback: (event: MouseEvent | TouchEvent) => void; - mode?: ClickOutsideMode; - listenerId: string; - hotkeyScope?: string; - enabled?: boolean; -}; - -export const useListenClickOutsideV2 = ({ - refs, - excludeClassNames, - callback, - mode = ClickOutsideMode.compareHTMLRef, - listenerId, - hotkeyScope, - enabled = true, -}: ClickOutsideListenerProps) => { - 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]); -}; diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx index 15f3caa60..0c5f63a04 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx @@ -3,7 +3,7 @@ import { OnDragEndResponder, ResponderProvided, } from '@hello-pangea/dnd'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { createPortal } from 'react-dom'; import { AppTooltip, @@ -20,7 +20,6 @@ import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableIt import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; import { isDefined } from '~/utils/isDefined'; @@ -80,15 +79,6 @@ export const ViewFieldsVisibilityDropdownSection = ({ return iconButtons.length ? iconButtons : undefined; }; - const ref = useRef(null); - - useListenClickOutside({ - refs: [ref], - callback: () => { - setOpenToolTipIndex(undefined); - }, - }); - const { nonDraggableItems = [], draggableItems = [] } = isDraggable ? groupArrayItemsBy(fields, ({ isLabelIdentifier }) => isLabelIdentifier ? 'nonDraggableItems' : 'draggableItems', @@ -96,7 +86,7 @@ export const ViewFieldsVisibilityDropdownSection = ({ : { nonDraggableItems: fields, draggableItems: [] }; return ( -
+ <> {showSubheader && ( {title} )} @@ -158,6 +148,6 @@ export const ViewFieldsVisibilityDropdownSection = ({ />, document.body, )} -
+ ); }; diff --git a/packages/twenty-ui/src/testing/index.ts b/packages/twenty-ui/src/testing/index.ts index dc67f4a8d..af19d7a08 100644 --- a/packages/twenty-ui/src/testing/index.ts +++ b/packages/twenty-ui/src/testing/index.ts @@ -5,3 +5,4 @@ export * from './decorators/ComponentWithRouterDecorator'; export * from './decorators/RouterDecorator'; export * from './mocks/avatarUrlMock'; export * from './types/CatalogStory'; +export * from './utils/getCanvasElementForDropdownTesting'; diff --git a/packages/twenty-ui/src/testing/utils/getCanvasElementForDropdownTesting.ts b/packages/twenty-ui/src/testing/utils/getCanvasElementForDropdownTesting.ts new file mode 100644 index 000000000..94c85f046 --- /dev/null +++ b/packages/twenty-ui/src/testing/utils/getCanvasElementForDropdownTesting.ts @@ -0,0 +1,15 @@ +import { isDefined } from '@ui/utilities'; + +export const getCanvasElementForDropdownTesting = () => { + const canvasElement = document.getElementsByClassName( + 'sb-show-main', + )[0] as HTMLElement | null; + + if (!isDefined(canvasElement)) { + throw new Error( + `Canvas element not found for dropdown testing in storybook, verify that storybook still uses the class name "sb-show-main" for its body that displays stories.`, + ); + } + + return canvasElement; +};