Refactor and fixes dropdown bugs (#8807)
Fixes https://github.com/twentyhq/twenty/issues/8788 Fixes https://github.com/twentyhq/twenty/issues/8793 Fixes https://github.com/twentyhq/twenty/issues/8791 Fixes https://github.com/twentyhq/twenty/issues/8890 Fixes https://github.com/twentyhq/twenty/issues/8893 - [x] Also : Icon buttons under dropdown are visible without blur :  - [x] Also : <img width="237" alt="image" src="https://github.com/user-attachments/assets/e4c70936-beff-4481-89cb-0a32a36e0ee2"> - [x] Also : <img width="335" alt="image" src="https://github.com/user-attachments/assets/5be60395-6baf-49eb-8d40-197add049e20"> - [x] Also : <img width="287" alt="image" src="https://github.com/user-attachments/assets/a317561f-7986-4d70-a1c0-deee4f4e268a"> - Button create new without padding - Container is expanding - [x] Also : <img width="303" alt="image" src="https://github.com/user-attachments/assets/09f8a27f-91db-4191-acdc-aaaeedaf6da5"> - [x] Also : <img width="133" alt="image" src="https://github.com/user-attachments/assets/fe17b32e-f7a4-46c4-8040-239eaf8198e8"> Font is cut at bottom ? - [x] Also : <img width="385" alt="image" src="https://github.com/user-attachments/assets/7bab2092-2936-4112-a2ee-d32d6737e304"> The component should flip and not resize in this situation - [x] Also : <img width="244" alt="image" src="https://github.com/user-attachments/assets/5384f49a-71f9-4638-a60c-158cc8c83f81"> - [x] Also : 
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
|
|||||||
@ -14,7 +14,12 @@ import {
|
|||||||
} from '@/action-menu/types/ActionMenuEntry';
|
} from '@/action-menu/types/ActionMenuEntry';
|
||||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
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 deleteMock = jest.fn();
|
||||||
const markAsDoneMock = jest.fn();
|
const markAsDoneMock = jest.fn();
|
||||||
@ -107,7 +112,9 @@ export const WithInteractions: Story = {
|
|||||||
args: {
|
args: {
|
||||||
actionMenuId: 'story',
|
actionMenuId: 'story',
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async () => {
|
||||||
|
const canvasElement = getCanvasElementForDropdownTesting();
|
||||||
|
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const deleteButton = await canvas.findByText('Delete');
|
const deleteButton = await canvas.findByText('Delete');
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
|||||||
import { userEvent, waitFor, within } from '@storybook/test';
|
import { userEvent, waitFor, within } from '@storybook/test';
|
||||||
import {
|
import {
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
|
getCanvasElementForDropdownTesting,
|
||||||
IconFileExport,
|
IconFileExport,
|
||||||
IconHeart,
|
IconHeart,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
@ -117,8 +118,8 @@ export const WithButtonClicks: Story = {
|
|||||||
args: {
|
args: {
|
||||||
actionMenuId: 'story-action-menu',
|
actionMenuId: 'story-action-menu',
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async () => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
let actionButton = await canvas.findByText('Actions');
|
let actionButton = await canvas.findByText('Actions');
|
||||||
await userEvent.click(actionButton);
|
await userEvent.click(actionButton);
|
||||||
@ -32,7 +32,7 @@ import { SelectableItem } from '@/ui/layout/selectable-list/components/Selectabl
|
|||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
@ -414,7 +414,7 @@ export const CommandMenu = () => {
|
|||||||
cmd.scope === CommandScope.Global,
|
cmd.scope === CommandScope.Global,
|
||||||
);
|
);
|
||||||
|
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [commandMenuRef],
|
refs: [commandMenuRef],
|
||||||
callback: closeCommandMenu,
|
callback: closeCommandMenu,
|
||||||
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
|||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
@ -167,43 +166,41 @@ export const ObjectFilterDropdownFilterSelect = ({
|
|||||||
setObjectFilterDropdownSearchInput(event.target.value)
|
setObjectFilterDropdownSearchInput(event.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<SelectableList
|
||||||
<SelectableList
|
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
|
||||||
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
|
selectableItemIdArray={selectableListItemIds}
|
||||||
selectableItemIdArray={selectableListItemIds}
|
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
|
||||||
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
|
onEnter={handleEnter}
|
||||||
onEnter={handleEnter}
|
>
|
||||||
>
|
<DropdownMenuItemsContainer>
|
||||||
<DropdownMenuItemsContainer>
|
{visibleColumnsFilterDefinitions.map(
|
||||||
{visibleColumnsFilterDefinitions.map(
|
(visibleFilterDefinition, index) => (
|
||||||
(visibleFilterDefinition, index) => (
|
<SelectableItem
|
||||||
<SelectableItem
|
itemId={visibleFilterDefinition.fieldMetadataId}
|
||||||
itemId={visibleFilterDefinition.fieldMetadataId}
|
key={`visible-select-filter-${index}`}
|
||||||
key={`visible-select-filter-${index}`}
|
>
|
||||||
>
|
<ObjectFilterDropdownFilterSelectMenuItem
|
||||||
<ObjectFilterDropdownFilterSelectMenuItem
|
filterDefinition={visibleFilterDefinition}
|
||||||
filterDefinition={visibleFilterDefinition}
|
/>
|
||||||
/>
|
</SelectableItem>
|
||||||
</SelectableItem>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
{shoudShowSeparator && <DropdownMenuSeparator />}
|
||||||
{shoudShowSeparator && <DropdownMenuSeparator />}
|
{hiddenColumnsFilterDefinitions.map(
|
||||||
{hiddenColumnsFilterDefinitions.map(
|
(hiddenFilterDefinition, index) => (
|
||||||
(hiddenFilterDefinition, index) => (
|
<SelectableItem
|
||||||
<SelectableItem
|
itemId={hiddenFilterDefinition.fieldMetadataId}
|
||||||
itemId={hiddenFilterDefinition.fieldMetadataId}
|
key={`hidden-select-filter-${index}`}
|
||||||
key={`hidden-select-filter-${index}`}
|
>
|
||||||
>
|
<ObjectFilterDropdownFilterSelectMenuItem
|
||||||
<ObjectFilterDropdownFilterSelectMenuItem
|
filterDefinition={hiddenFilterDefinition}
|
||||||
filterDefinition={hiddenFilterDefinition}
|
/>
|
||||||
/>
|
</SelectableItem>
|
||||||
</SelectableItem>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenuItemsContainer>
|
</SelectableList>
|
||||||
</SelectableList>
|
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
|
||||||
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
|
|
||||||
</ScrollWrapper>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -28,9 +28,11 @@ export const ObjectFilterDropdownOperandDropdown = ({
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const dropdownId = `${filterDropdownId}-operand-dropdown`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={`${filterDropdownId}-operand-dropdown`}
|
dropdownId={dropdownId}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<StyledDropdownMenuHeader
|
<StyledDropdownMenuHeader
|
||||||
key={'selected-filter-operand'}
|
key={'selected-filter-operand'}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/inter
|
|||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|
||||||
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui';
|
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -163,23 +162,21 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
{optionsInDropdown?.map((option) => (
|
||||||
{optionsInDropdown?.map((option) => (
|
<MenuItemMultiSelect
|
||||||
<MenuItemMultiSelect
|
key={option.id}
|
||||||
key={option.id}
|
selected={option.isSelected}
|
||||||
selected={option.isSelected}
|
isKeySelected={option.id === selectedItemId}
|
||||||
isKeySelected={option.id === selectedItemId}
|
onSelectChange={(selected) =>
|
||||||
onSelectChange={(selected) =>
|
handleMultipleOptionSelectChange(option, selected)
|
||||||
handleMultipleOptionSelectChange(option, selected)
|
}
|
||||||
}
|
text={option.label}
|
||||||
text={option.label}
|
color={option.color}
|
||||||
color={option.color}
|
className=""
|
||||||
className=""
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</ScrollWrapper>
|
|
||||||
{showNoResult && <MenuItem text="No result" />}
|
{showNoResult && <MenuItem text="No result" />}
|
||||||
</SelectableList>
|
</SelectableList>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,7 +11,10 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
|
|||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
import { within } from '@storybook/test';
|
import { within } from '@storybook/test';
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
import {
|
||||||
|
ComponentDecorator,
|
||||||
|
getCanvasElementForDropdownTesting,
|
||||||
|
} from 'twenty-ui';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
|
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
|
||||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
@ -120,7 +123,7 @@ type Story = StoryObj<typeof TaskGroups>;
|
|||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
play: async () => {
|
play: async () => {
|
||||||
const canvas = within(document.body);
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
const filterButton = await canvas.findByText('Filter');
|
const filterButton = await canvas.findByText('Filter');
|
||||||
|
|
||||||
@ -142,7 +145,7 @@ export const Default: Story = {
|
|||||||
|
|
||||||
export const Date: Story = {
|
export const Date: Story = {
|
||||||
play: async () => {
|
play: async () => {
|
||||||
const canvas = within(document.body);
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
const filterButton = await canvas.findByText('Filter');
|
const filterButton = await canvas.findByText('Filter');
|
||||||
|
|
||||||
@ -156,7 +159,7 @@ export const Date: Story = {
|
|||||||
|
|
||||||
export const Number: Story = {
|
export const Number: Story = {
|
||||||
play: async () => {
|
play: async () => {
|
||||||
const canvas = within(document.body);
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
const filterButton = await canvas.findByText('Filter');
|
const filterButton = await canvas.findByText('Filter');
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hook
|
|||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|
||||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
|
||||||
@ -53,19 +52,17 @@ export const ObjectOptionsDropdownFieldsContent = () => {
|
|||||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
|
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
|
||||||
Fields
|
Fields
|
||||||
</DropdownMenuHeader>
|
</DropdownMenuHeader>
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<ViewFieldsVisibilityDropdownSection
|
||||||
<ViewFieldsVisibilityDropdownSection
|
title="Visible"
|
||||||
title="Visible"
|
fields={visibleRecordFields}
|
||||||
fields={visibleRecordFields}
|
isDraggable
|
||||||
isDraggable
|
onDragEnd={handleReorderFields}
|
||||||
onDragEnd={handleReorderFields}
|
onVisibilityChange={handleChangeFieldVisibility}
|
||||||
onVisibilityChange={handleChangeFieldVisibility}
|
showSubheader={false}
|
||||||
showSubheader={false}
|
showDragGrip={true}
|
||||||
showDragGrip={true}
|
/>
|
||||||
/>
|
|
||||||
</ScrollWrapper>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||||
<MenuItemNavigate
|
<MenuItemNavigate
|
||||||
onClick={() => onContentChange('hiddenFields')}
|
onClick={() => onContentChange('hiddenFields')}
|
||||||
LeftIcon={IconEyeOff}
|
LeftIcon={IconEyeOff}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
|
|||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|
||||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
|
||||||
@ -71,19 +70,16 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
|
|||||||
Hidden Fields
|
Hidden Fields
|
||||||
</DropdownMenuHeader>
|
</DropdownMenuHeader>
|
||||||
{hiddenRecordFields.length > 0 && (
|
{hiddenRecordFields.length > 0 && (
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<ViewFieldsVisibilityDropdownSection
|
||||||
<ViewFieldsVisibilityDropdownSection
|
title="Hidden"
|
||||||
title="Hidden"
|
fields={hiddenRecordFields}
|
||||||
fields={hiddenRecordFields}
|
isDraggable={false}
|
||||||
isDraggable={false}
|
onVisibilityChange={handleChangeFieldVisibility}
|
||||||
onVisibilityChange={handleChangeFieldVisibility}
|
showSubheader={false}
|
||||||
showSubheader={false}
|
showDragGrip={false}
|
||||||
showDragGrip={false}
|
/>
|
||||||
/>
|
|
||||||
</ScrollWrapper>
|
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<UndecoratedLink
|
<UndecoratedLink
|
||||||
to={settingsUrl}
|
to={settingsUrl}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -91,7 +87,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
|
|||||||
closeDropdown();
|
closeDropdown();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||||
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
|
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</UndecoratedLink>
|
</UndecoratedLink>
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
|||||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { SORT_DIRECTIONS } from '../types/SortDirection';
|
import { SORT_DIRECTIONS } from '../types/SortDirection';
|
||||||
@ -184,39 +183,37 @@ export const ObjectSortDropdownButton = ({
|
|||||||
setObjectSortDropdownSearchInput(event.target.value)
|
setObjectSortDropdownSearchInput(event.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<DropdownMenuItemsContainer>
|
||||||
<DropdownMenuItemsContainer>
|
{visibleColumnsSortDefinitions.map(
|
||||||
{visibleColumnsSortDefinitions.map(
|
(visibleSortDefinition, index) => (
|
||||||
(visibleSortDefinition, index) => (
|
<MenuItem
|
||||||
<MenuItem
|
testId={`visible-select-sort-${index}`}
|
||||||
testId={`visible-select-sort-${index}`}
|
key={index}
|
||||||
key={index}
|
onClick={() => {
|
||||||
onClick={() => {
|
setObjectSortDropdownSearchInput('');
|
||||||
setObjectSortDropdownSearchInput('');
|
handleAddSort(visibleSortDefinition);
|
||||||
handleAddSort(visibleSortDefinition);
|
}}
|
||||||
}}
|
LeftIcon={getIcon(visibleSortDefinition.iconName)}
|
||||||
LeftIcon={getIcon(visibleSortDefinition.iconName)}
|
text={visibleSortDefinition.label}
|
||||||
text={visibleSortDefinition.label}
|
/>
|
||||||
/>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
{shoudShowSeparator && <DropdownMenuSeparator />}
|
||||||
{shoudShowSeparator && <DropdownMenuSeparator />}
|
{hiddenColumnsSortDefinitions.map(
|
||||||
{hiddenColumnsSortDefinitions.map(
|
(hiddenSortDefinition, index) => (
|
||||||
(hiddenSortDefinition, index) => (
|
<MenuItem
|
||||||
<MenuItem
|
testId={`hidden-select-sort-${index}`}
|
||||||
testId={`hidden-select-sort-${index}`}
|
key={index}
|
||||||
key={index}
|
onClick={() => {
|
||||||
onClick={() => {
|
setObjectSortDropdownSearchInput('');
|
||||||
setObjectSortDropdownSearchInput('');
|
handleAddSort(hiddenSortDefinition);
|
||||||
handleAddSort(hiddenSortDefinition);
|
}}
|
||||||
}}
|
LeftIcon={getIcon(hiddenSortDefinition.iconName)}
|
||||||
LeftIcon={getIcon(hiddenSortDefinition.iconName)}
|
text={hiddenSortDefinition.label}
|
||||||
text={hiddenSortDefinition.label}
|
/>
|
||||||
/>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</ScrollWrapper>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onClose={handleDropdownButtonClose}
|
onClose={handleDropdownButtonClose}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
|||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
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 { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
@ -81,7 +81,7 @@ export const RecordBoard = () => {
|
|||||||
const { resetRecordSelection, setRecordAsSelected } =
|
const { resetRecordSelection, setRecordAsSelected } =
|
||||||
useRecordBoardSelection(recordBoardId);
|
useRecordBoardSelection(recordBoardId);
|
||||||
|
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
excludeClassNames: [
|
excludeClassNames: [
|
||||||
'bottom-bar',
|
'bottom-bar',
|
||||||
'action-menu-dropdown',
|
'action-menu-dropdown',
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export const RecordBoardColumnDropdownMenu = ({
|
|||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [boardColumnMenuRef],
|
refs: [boardColumnMenuRef],
|
||||||
callback: closeMenu,
|
callback: closeMenu,
|
||||||
|
listenerId: 'record-board-column-dropdown-menu',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -48,7 +48,10 @@ type FieldInputProps = {
|
|||||||
recordFieldInputdId: string;
|
recordFieldInputdId: string;
|
||||||
onSubmit?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: (
|
||||||
|
persist: () => void,
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
) => void;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -78,7 +81,10 @@ export const FieldInput = ({
|
|||||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||||
) : isFieldPhones(fieldDefinition) ? (
|
) : isFieldPhones(fieldDefinition) ? (
|
||||||
<PhonesFieldInput onCancel={onCancel} />
|
<PhonesFieldInput
|
||||||
|
onCancel={onCancel}
|
||||||
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
|
/>
|
||||||
) : isFieldText(fieldDefinition) ? (
|
) : isFieldText(fieldDefinition) ? (
|
||||||
<TextFieldInput
|
<TextFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
@ -88,7 +94,10 @@ export const FieldInput = ({
|
|||||||
onShiftTab={onShiftTab}
|
onShiftTab={onShiftTab}
|
||||||
/>
|
/>
|
||||||
) : isFieldEmails(fieldDefinition) ? (
|
) : isFieldEmails(fieldDefinition) ? (
|
||||||
<EmailsFieldInput onCancel={onCancel} />
|
<EmailsFieldInput
|
||||||
|
onCancel={onCancel}
|
||||||
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
|
/>
|
||||||
) : isFieldFullName(fieldDefinition) ? (
|
) : isFieldFullName(fieldDefinition) ? (
|
||||||
<FullNameFieldInput
|
<FullNameFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
@ -122,7 +131,10 @@ export const FieldInput = ({
|
|||||||
onShiftTab={onShiftTab}
|
onShiftTab={onShiftTab}
|
||||||
/>
|
/>
|
||||||
) : isFieldLinks(fieldDefinition) ? (
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
<LinksFieldInput onCancel={onCancel} />
|
<LinksFieldInput
|
||||||
|
onCancel={onCancel}
|
||||||
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
|
/>
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
<CurrencyFieldInput
|
<CurrencyFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
@ -158,7 +170,10 @@ export const FieldInput = ({
|
|||||||
) : isFieldRichText(fieldDefinition) ? (
|
) : isFieldRichText(fieldDefinition) ? (
|
||||||
<RichTextFieldInput />
|
<RichTextFieldInput />
|
||||||
) : isFieldArray(fieldDefinition) ? (
|
) : isFieldArray(fieldDefinition) ? (
|
||||||
<ArrayFieldInput onCancel={onCancel} />
|
<ArrayFieldInput
|
||||||
|
onCancel={onCancel}
|
||||||
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -108,9 +108,6 @@ export const FormSelectFieldInput = ({
|
|||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [selectWrapperRef, setSelectWrapperRef] =
|
|
||||||
useState<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
|
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
|
||||||
|
|
||||||
const { resetSelectedItem } = useSelectableList(
|
const { resetSelectedItem } = useSelectableList(
|
||||||
@ -206,7 +203,6 @@ export const FormSelectFieldInput = ({
|
|||||||
|
|
||||||
<StyledFormFieldInputRowContainer>
|
<StyledFormFieldInputRowContainer>
|
||||||
<StyledFormFieldInputInputContainer
|
<StyledFormFieldInputInputContainer
|
||||||
ref={setSelectWrapperRef}
|
|
||||||
hasRightElement={isDefined(VariablePicker)}
|
hasRightElement={isDefined(VariablePicker)}
|
||||||
>
|
>
|
||||||
{draftValue.type === 'static' ? (
|
{draftValue.type === 'static' ? (
|
||||||
@ -231,7 +227,6 @@ export const FormSelectFieldInput = ({
|
|||||||
selectableItemIdArray={optionIds}
|
selectableItemIdArray={optionIds}
|
||||||
hotkeyScope={hotkeyScope}
|
hotkeyScope={hotkeyScope}
|
||||||
onEnter={handleSelectEnter}
|
onEnter={handleSelectEnter}
|
||||||
selectWrapperRef={selectWrapperRef}
|
|
||||||
onOptionSelected={handleSubmit}
|
onOptionSelected={handleSubmit}
|
||||||
options={field.metadata.options}
|
options={field.metadata.options}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
|||||||
@ -4,10 +4,13 @@ import { AddressInput } from '@/ui/field/input/components/AddressInput';
|
|||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import {
|
||||||
|
FieldInputClickOutsideEvent,
|
||||||
|
FieldInputEvent,
|
||||||
|
} from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type AddressFieldInputProps = {
|
export type AddressFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -60,7 +63,7 @@ export const AddressFieldInput = ({
|
|||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newAddress: FieldAddressDraftValue,
|
newAddress: FieldAddressDraftValue,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => persistField(convertToAddress(newAddress)));
|
onClickOutside?.(() => persistField(convertToAddress(newAddress)), event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (newAddress: FieldAddressDraftValue) => {
|
const handleChange = (newAddress: FieldAddressDraftValue) => {
|
||||||
|
|||||||
@ -6,9 +6,13 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
type ArrayFieldInputProps = {
|
type ArrayFieldInputProps = {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => {
|
export const ArrayFieldInput = ({
|
||||||
|
onCancel,
|
||||||
|
onClickOutside,
|
||||||
|
}: ArrayFieldInputProps) => {
|
||||||
const { persistArrayField, hotkeyScope, fieldValue } = useArrayField();
|
const { persistArrayField, hotkeyScope, fieldValue } = useArrayField();
|
||||||
|
|
||||||
const arrayItems = useMemo<Array<string>>(
|
const arrayItems = useMemo<Array<string>>(
|
||||||
@ -23,6 +27,7 @@ export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => {
|
|||||||
items={arrayItems}
|
items={arrayItems}
|
||||||
onPersist={persistArrayField}
|
onPersist={persistArrayField}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
fieldMetadataType={FieldMetadataType.Array}
|
fieldMetadataType={FieldMetadataType.Array}
|
||||||
renderItem={({ value, index, handleEdit, handleDelete }) => (
|
renderItem={({ value, index, handleEdit, handleDelete }) => (
|
||||||
|
|||||||
@ -8,10 +8,13 @@ import { FieldInputOverlay } from '../../../../../ui/field/input/components/Fiel
|
|||||||
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
||||||
|
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import {
|
||||||
|
FieldInputClickOutsideEvent,
|
||||||
|
FieldInputEvent,
|
||||||
|
} from './DateTimeFieldInput';
|
||||||
|
|
||||||
type CurrencyFieldInputProps = {
|
type CurrencyFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -79,7 +82,7 @@ export const CurrencyFieldInput = ({
|
|||||||
amountText: newValue,
|
amountText: newValue,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
});
|
});
|
||||||
});
|
}, event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTab = (newValue: string) => {
|
const handleTab = (newValue: string) => {
|
||||||
|
|||||||
@ -5,11 +5,12 @@ import { DateInput } from '@/ui/field/input/components/DateInput';
|
|||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||||
|
|
||||||
type FieldInputEvent = (persist: () => void) => void;
|
type FieldInputEvent = (persist: () => void) => void;
|
||||||
|
|
||||||
type DateFieldInputProps = {
|
type DateFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onClear?: FieldInputEvent;
|
onClear?: FieldInputEvent;
|
||||||
@ -50,10 +51,10 @@ export const DateFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = (
|
const handleClickOutside = (
|
||||||
_event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newDate: Nullable<Date>,
|
newDate: Nullable<Date>,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => persistDate(newDate));
|
onClickOutside?.(() => persistDate(newDate), event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (newDate: Nullable<Date>) => {
|
const handleChange = (newDate: Nullable<Date>) => {
|
||||||
|
|||||||
@ -6,9 +6,13 @@ import { usePersistField } from '../../../hooks/usePersistField';
|
|||||||
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
||||||
|
|
||||||
export type FieldInputEvent = (persist: () => void) => void;
|
export type FieldInputEvent = (persist: () => void) => void;
|
||||||
|
export type FieldInputClickOutsideEvent = (
|
||||||
|
persist: () => void,
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
) => void;
|
||||||
|
|
||||||
export type DateTimeFieldInputProps = {
|
export type DateTimeFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onClear?: FieldInputEvent;
|
onClear?: FieldInputEvent;
|
||||||
@ -45,10 +49,10 @@ export const DateTimeFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = (
|
const handleClickOutside = (
|
||||||
_event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newDate: Nullable<Date>,
|
newDate: Nullable<Date>,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => persistDate(newDate));
|
onClickOutside?.(() => persistDate(newDate), event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (newDate: Nullable<Date>) => {
|
const handleChange = (newDate: Nullable<Date>) => {
|
||||||
|
|||||||
@ -8,9 +8,13 @@ import { MultiItemFieldInput } from './MultiItemFieldInput';
|
|||||||
|
|
||||||
type EmailsFieldInputProps = {
|
type EmailsFieldInputProps = {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
|
export const EmailsFieldInput = ({
|
||||||
|
onCancel,
|
||||||
|
onClickOutside,
|
||||||
|
}: EmailsFieldInputProps) => {
|
||||||
const { persistEmailsField, hotkeyScope, fieldValue } = useEmailsField();
|
const { persistEmailsField, hotkeyScope, fieldValue } = useEmailsField();
|
||||||
|
|
||||||
const emails = useMemo<string[]>(
|
const emails = useMemo<string[]>(
|
||||||
@ -45,6 +49,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
|
|||||||
items={emails}
|
items={emails}
|
||||||
onPersist={handlePersistEmails}
|
onPersist={handlePersistEmails}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
fieldMetadataType={FieldMetadataType.Emails}
|
fieldMetadataType={FieldMetadataType.Emails}
|
||||||
validateInput={validateInput}
|
validateInput={validateInput}
|
||||||
|
|||||||
@ -4,7 +4,10 @@ import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
|
|||||||
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
|
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
|
||||||
|
|
||||||
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
|
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 =
|
const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
|
||||||
'First name';
|
'First name';
|
||||||
@ -13,7 +16,7 @@ const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS =
|
|||||||
'Last name';
|
'Last name';
|
||||||
|
|
||||||
type FullNameFieldInputProps = {
|
type FullNameFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -57,8 +60,9 @@ export const FullNameFieldInput = ({
|
|||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newDoubleText: FieldDoubleText,
|
newDoubleText: FieldDoubleText,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() =>
|
onClickOutside?.(
|
||||||
persistFullNameField(convertToFullName(newDoubleText)),
|
() => persistFullNameField(convertToFullName(newDoubleText)),
|
||||||
|
event,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,13 @@ import { MultiItemFieldInput } from './MultiItemFieldInput';
|
|||||||
|
|
||||||
type LinksFieldInputProps = {
|
type LinksFieldInputProps = {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
|
export const LinksFieldInput = ({
|
||||||
|
onCancel,
|
||||||
|
onClickOutside,
|
||||||
|
}: LinksFieldInputProps) => {
|
||||||
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();
|
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();
|
||||||
|
|
||||||
const links = useMemo<{ url: string; label: string }[]>(
|
const links = useMemo<{ url: string; label: string }[]>(
|
||||||
@ -49,6 +53,7 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
|
|||||||
items={links}
|
items={links}
|
||||||
onPersist={handlePersistLinks}
|
onPersist={handlePersistLinks}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
fieldMetadataType={FieldMetadataType.Links}
|
fieldMetadataType={FieldMetadataType.Links}
|
||||||
validateInput={(input) => ({
|
validateInput={(input) => ({
|
||||||
|
|||||||
@ -41,6 +41,7 @@ type MultiItemFieldInputProps<T> = {
|
|||||||
newItemLabel?: string;
|
newItemLabel?: string;
|
||||||
fieldMetadataType: FieldMetadataType;
|
fieldMetadataType: FieldMetadataType;
|
||||||
renderInput?: DropdownMenuInputProps['renderInput'];
|
renderInput?: DropdownMenuInputProps['renderInput'];
|
||||||
|
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
|
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
|
||||||
@ -57,24 +58,19 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
newItemLabel,
|
newItemLabel,
|
||||||
fieldMetadataType,
|
fieldMetadataType,
|
||||||
renderInput,
|
renderInput,
|
||||||
|
onClickOutside,
|
||||||
}: MultiItemFieldInputProps<T>) => {
|
}: MultiItemFieldInputProps<T>) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const handleDropdownClose = () => {
|
const handleDropdownClose = () => {
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => {
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
if (inputValue?.trim().length > 0) {
|
|
||||||
handleSubmitInput();
|
|
||||||
} else {
|
|
||||||
onCancel?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [containerRef],
|
||||||
callback: handleDropdownCloseOutside,
|
callback: (event) => {
|
||||||
|
onClickOutside?.(event);
|
||||||
|
},
|
||||||
|
listenerId: hotkeyScope,
|
||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
|
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
|
import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
|
||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconBookmarkPlus,
|
IconBookmarkPlus,
|
||||||
@ -37,23 +37,29 @@ export const MultiItemFieldMenuItem = <T,>({
|
|||||||
const handleMouseEnter = () => setIsHovered(true);
|
const handleMouseEnter = () => setIsHovered(true);
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
if (isDropdownOpen) {
|
|
||||||
closeDropdown();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
onDelete?.();
|
onDelete?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetAsPrimaryClick = () => {
|
const handleSetAsPrimaryClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
onSetAsPrimary?.();
|
onSetAsPrimary?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditClick = () => {
|
const handleEditClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
onEdit?.();
|
onEdit?.();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -88,6 +88,7 @@ export const MultiSelectFieldInput = ({
|
|||||||
}
|
}
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
},
|
},
|
||||||
|
listenerId: 'MultiSelectFieldInput',
|
||||||
});
|
});
|
||||||
|
|
||||||
const optionIds = filteredOptionsInDropDown.map((option) => option.value);
|
const optionIds = filteredOptionsInDropDown.map((option) => option.value);
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import { TextInput } from '@/ui/field/input/components/TextInput';
|
|||||||
|
|
||||||
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
|
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
|
||||||
import { useNumberField } from '../../hooks/useNumberField';
|
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 FieldInputEvent = (persist: () => void) => void;
|
||||||
|
|
||||||
export type NumberFieldInputProps = {
|
export type NumberFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -40,7 +41,7 @@ export const NumberFieldInput = ({
|
|||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newText: string,
|
newText: string,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => persistNumberField(newText));
|
onClickOutside?.(() => persistNumberField(newText), event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTab = (newText: string) => {
|
const handleTab = (newText: string) => {
|
||||||
|
|||||||
@ -49,10 +49,14 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
|||||||
|
|
||||||
type PhonesFieldInputProps = {
|
type PhonesFieldInputProps = {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
|
export const PhonesFieldInput = ({
|
||||||
const { persistPhonesField, hotkeyScope, fieldDefinition, fieldValue } =
|
onCancel,
|
||||||
|
onClickOutside,
|
||||||
|
}: PhonesFieldInputProps) => {
|
||||||
|
const { persistPhonesField, hotkeyScope, fieldValue, fieldDefinition } =
|
||||||
usePhonesField();
|
usePhonesField();
|
||||||
|
|
||||||
const phones = createPhonesFromFieldValue(fieldValue);
|
const phones = createPhonesFromFieldValue(fieldValue);
|
||||||
@ -83,6 +87,7 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => {
|
|||||||
<MultiItemFieldInput
|
<MultiItemFieldInput
|
||||||
items={phones}
|
items={phones}
|
||||||
onPersist={handlePersistPhones}
|
onPersist={handlePersistPhones}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
placeholder="Phone"
|
placeholder="Phone"
|
||||||
fieldMetadataType={FieldMetadataType.Phones}
|
fieldMetadataType={FieldMetadataType.Phones}
|
||||||
|
|||||||
@ -3,10 +3,13 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
|||||||
|
|
||||||
import { useJsonField } from '../../hooks/useJsonField';
|
import { useJsonField } from '../../hooks/useJsonField';
|
||||||
|
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import {
|
||||||
|
FieldInputClickOutsideEvent,
|
||||||
|
FieldInputEvent,
|
||||||
|
} from './DateTimeFieldInput';
|
||||||
|
|
||||||
type RawJsonFieldInputProps = {
|
type RawJsonFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -37,10 +40,10 @@ export const RawJsonFieldInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = (
|
const handleClickOutside = (
|
||||||
_event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newText: string,
|
newText: string,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => persistJsonField(newText));
|
onClickOutside?.(() => persistJsonField(newText), event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTab = (newText: string) => {
|
const handleTab = (newText: string) => {
|
||||||
|
|||||||
@ -21,8 +21,6 @@ export const SelectFieldInput = ({
|
|||||||
}: SelectFieldInputProps) => {
|
}: SelectFieldInputProps) => {
|
||||||
const { persistField, fieldDefinition, fieldValue, hotkeyScope } =
|
const { persistField, fieldDefinition, fieldValue, hotkeyScope } =
|
||||||
useSelectField();
|
useSelectField();
|
||||||
const [selectWrapperRef, setSelectWrapperRef] =
|
|
||||||
useState<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
|
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
|
||||||
|
|
||||||
@ -62,7 +60,7 @@ export const SelectFieldInput = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setSelectWrapperRef}>
|
<div>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
|
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
|
||||||
selectableItemIdArray={optionIds}
|
selectableItemIdArray={optionIds}
|
||||||
@ -76,7 +74,6 @@ export const SelectFieldInput = ({
|
|||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
selectWrapperRef={selectWrapperRef}
|
|
||||||
onOptionSelected={handleSubmit}
|
onOptionSelected={handleSubmit}
|
||||||
options={fieldDefinition.metadata.options}
|
options={fieldDefinition.metadata.options}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
|||||||
@ -5,10 +5,13 @@ import { usePersistField } from '../../../hooks/usePersistField';
|
|||||||
import { useTextField } from '../../hooks/useTextField';
|
import { useTextField } from '../../hooks/useTextField';
|
||||||
|
|
||||||
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
|
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import {
|
||||||
|
FieldInputClickOutsideEvent,
|
||||||
|
FieldInputEvent,
|
||||||
|
} from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type TextFieldInputProps = {
|
export type TextFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
onTab?: FieldInputEvent;
|
||||||
@ -38,7 +41,7 @@ export const TextFieldInput = ({
|
|||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
newText: string,
|
newText: string,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => persistField(newText.trim()));
|
onClickOutside?.(() => persistField(newText.trim()), event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTab = (newText: string) => {
|
const handleTab = (newText: string) => {
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import { Decorator, Meta, StoryObj } from '@storybook/react';
|
import { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||||
import {
|
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
expect,
|
|
||||||
fireEvent,
|
|
||||||
fn,
|
|
||||||
userEvent,
|
|
||||||
waitFor,
|
|
||||||
within,
|
|
||||||
} from '@storybook/test';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
@ -26,6 +19,7 @@ import {
|
|||||||
|
|
||||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||||
|
import { getCanvasElementForDropdownTesting } from 'twenty-ui';
|
||||||
import {
|
import {
|
||||||
RelationToOneFieldInput,
|
RelationToOneFieldInput,
|
||||||
RelationToOneFieldInputProps,
|
RelationToOneFieldInputProps,
|
||||||
@ -136,8 +130,8 @@ export const Default: Story = {
|
|||||||
|
|
||||||
export const Submit: Story = {
|
export const Submit: Story = {
|
||||||
decorators: [ComponentWithRecoilScopeDecorator],
|
decorators: [ComponentWithRecoilScopeDecorator],
|
||||||
play: async ({ canvasElement }) => {
|
play: async () => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
@ -154,16 +148,16 @@ export const Submit: Story = {
|
|||||||
|
|
||||||
export const Cancel: Story = {
|
export const Cancel: Story = {
|
||||||
decorators: [ComponentWithRecoilScopeDecorator],
|
decorators: [ComponentWithRecoilScopeDecorator],
|
||||||
play: async ({ canvasElement }) => {
|
play: async () => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(getCanvasElementForDropdownTesting());
|
||||||
|
|
||||||
expect(cancelJestFn).toHaveBeenCalledTimes(0);
|
expect(cancelJestFn).toHaveBeenCalledTimes(0);
|
||||||
await canvas.findByText('John Wick', undefined, { timeout: 3000 });
|
await canvas.findByText('John Wick', undefined, { timeout: 3000 });
|
||||||
|
|
||||||
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
|
||||||
fireEvent.click(emptyDiv);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
userEvent.click(emptyDiv);
|
||||||
expect(cancelJestFn).toHaveBeenCalledTimes(1);
|
expect(cancelJestFn).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,10 +28,10 @@ export const useRegisterInputEvents = <T>({
|
|||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [inputRef, copyRef].filter(isDefined),
|
refs: [inputRef, copyRef].filter(isDefined),
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
event.stopImmediatePropagation();
|
|
||||||
onClickOutside?.(event, inputValue);
|
onClickOutside?.(event, inputValue);
|
||||||
},
|
},
|
||||||
enabled: isDefined(onClickOutside),
|
enabled: isDefined(onClickOutside),
|
||||||
|
listenerId: hotkeyScope,
|
||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
|
|||||||
@ -14,7 +14,11 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
|
|||||||
import { useInlineCell } from '../hooks/useInlineCell';
|
import { useInlineCell } from '../hooks/useInlineCell';
|
||||||
|
|
||||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
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 { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||||
|
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
||||||
import {
|
import {
|
||||||
RecordInlineCellContext,
|
RecordInlineCellContext,
|
||||||
@ -65,10 +69,30 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
|||||||
closeInlineCell();
|
closeInlineCell();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside: FieldInputEvent = (persistField) => {
|
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
|
||||||
persistField();
|
({ snapshot }) =>
|
||||||
closeInlineCell();
|
(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();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,10 @@ const StyledInlineCellInput = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
min-width: 240px;
|
|
||||||
|
|
||||||
width: inherit;
|
width: 240px;
|
||||||
|
|
||||||
z-index: 1000;
|
z-index: 30;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RecordInlineCellEditModeProps = {
|
type RecordInlineCellEditModeProps = {
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
|||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
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 { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState';
|
||||||
import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope';
|
||||||
|
|
||||||
@ -21,6 +24,11 @@ export const useInlineCell = () => {
|
|||||||
isInlineCellInEditModeScopedState(recoilScopeId),
|
isInlineCellInEditModeScopedState(recoilScopeId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||||
|
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||||
|
const { goBackToPreviousDropdownFocusId } =
|
||||||
|
useGoBackToPreviousDropdownFocusId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
@ -32,6 +40,8 @@ export const useInlineCell = () => {
|
|||||||
setIsInlineCellInEditMode(false);
|
setIsInlineCellInEditMode(false);
|
||||||
|
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
|
|
||||||
|
goBackToPreviousDropdownFocusId();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
||||||
@ -46,6 +56,14 @@ export const useInlineCell = () => {
|
|||||||
} else {
|
} else {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell);
|
setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveDropdownFocusIdAndMemorizePrevious(
|
||||||
|
getDropdownFocusIdForRecordField(
|
||||||
|
recordId,
|
||||||
|
fieldDefinition.fieldMetadataId,
|
||||||
|
'inline-cell',
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
|
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
|
||||||
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
|
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 { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
|
|
||||||
@ -16,6 +17,9 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => {
|
|||||||
recordTableId,
|
recordTableId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { goBackToPreviousDropdownFocusId } =
|
||||||
|
useGoBackToPreviousDropdownFocusId();
|
||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
({ set, snapshot }) => {
|
({ set, snapshot }) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
@ -28,8 +32,14 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => {
|
|||||||
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
goBackToPreviousDropdownFocusId();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState],
|
[
|
||||||
|
currentTableCellInEditModePositionState,
|
||||||
|
isTableCellInEditModeFamilyState,
|
||||||
|
goBackToPreviousDropdownFocusId,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/
|
|||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
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 = {
|
type RecordTableBodyUnselectEffectProps = {
|
||||||
tableBodyRef: React.RefObject<HTMLDivElement>;
|
tableBodyRef: React.RefObject<HTMLDivElement>;
|
||||||
@ -32,7 +32,7 @@ export const RecordTableBodyUnselectEffect = ({
|
|||||||
TableHotkeyScope.Table,
|
TableHotkeyScope.Table,
|
||||||
);
|
);
|
||||||
|
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
excludeClassNames: [
|
excludeClassNames: [
|
||||||
'bottom-bar',
|
'bottom-bar',
|
||||||
'action-menu-dropdown',
|
'action-menu-dropdown',
|
||||||
|
|||||||
@ -4,19 +4,13 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
|||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
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 { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
|
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
export const RecordTableCellFieldInput = () => {
|
export const RecordTableCellFieldInput = () => {
|
||||||
const { getClickOutsideListenerIsActivatedState } =
|
|
||||||
useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID);
|
|
||||||
const setClickOutsideListenerIsActivated = useSetRecoilState(
|
|
||||||
getClickOutsideListenerIsActivatedState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
|
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
|
||||||
useContext(RecordTableContext);
|
useContext(RecordTableContext);
|
||||||
|
|
||||||
@ -48,17 +42,35 @@ export const RecordTableCellFieldInput = () => {
|
|||||||
onCloseTableCell();
|
onCloseTableCell();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside: FieldInputEvent = (persistField) => {
|
const handleClickOutside = useRecoilCallback(
|
||||||
setClickOutsideListenerIsActivated(false);
|
({ snapshot }) =>
|
||||||
|
(persistField: () => void, event: MouseEvent | TouchEvent) => {
|
||||||
|
const dropdownFocusId = getDropdownFocusIdForRecordField(
|
||||||
|
recordId,
|
||||||
|
fieldDefinition.fieldMetadataId,
|
||||||
|
'table-cell',
|
||||||
|
);
|
||||||
|
|
||||||
onUpsertRecord({
|
const activeDropdownFocusId = snapshot
|
||||||
persistField,
|
.getLoadable(activeDropdownFocusIdState)
|
||||||
recordId,
|
.getValue();
|
||||||
fieldName: fieldDefinition.metadata.fieldName,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCloseTableCell();
|
if (activeDropdownFocusId !== dropdownFocusId) {
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
onUpsertRecord({
|
||||||
|
persistField,
|
||||||
|
recordId,
|
||||||
|
fieldName: fieldDefinition.metadata.fieldName,
|
||||||
|
});
|
||||||
|
|
||||||
|
onCloseTableCell();
|
||||||
|
},
|
||||||
|
[fieldDefinition, onCloseTableCell, onUpsertRecord, recordId],
|
||||||
|
);
|
||||||
|
|
||||||
const handleEscape: FieldInputEvent = (persistField) => {
|
const handleEscape: FieldInputEvent = (persistField) => {
|
||||||
onUpsertRecord({
|
onUpsertRecord({
|
||||||
|
|||||||
@ -10,13 +10,11 @@ import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEm
|
|||||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||||
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
|
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
|
||||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
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 { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton';
|
||||||
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
|
||||||
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
|
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||||
@ -34,7 +32,6 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
nonEditModeContent,
|
nonEditModeContent,
|
||||||
}: RecordTableCellSoftFocusModeProps) => {
|
}: RecordTableCellSoftFocusModeProps) => {
|
||||||
const { columnIndex } = useContext(RecordTableCellContext);
|
const { columnIndex } = useContext(RecordTableCellContext);
|
||||||
const closeCurrentTableCell = useCloseCurrentTableCellInEditMode();
|
|
||||||
|
|
||||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||||
|
|
||||||
@ -135,13 +132,6 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
useListenClickOutside({
|
|
||||||
refs: [scrollRef],
|
|
||||||
callback: () => {
|
|
||||||
closeCurrentTableCell();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isFirstColumn = columnIndex === 0;
|
const isFirstColumn = columnIndex === 0;
|
||||||
const customButtonIcon = useGetButtonIcon();
|
const customButtonIcon = useGetButtonIcon();
|
||||||
const buttonIcon = isFirstColumn
|
const buttonIcon = isFirstColumn
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
|
|
||||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
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 { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -69,6 +71,9 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||||
|
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||||
|
|
||||||
const openTableCell = useRecoilCallback(
|
const openTableCell = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
({
|
({
|
||||||
@ -142,6 +147,14 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
|||||||
DEFAULT_CELL_SCOPE.customScopes,
|
DEFAULT_CELL_SCOPE.customScopes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveDropdownFocusIdAndMemorizePrevious(
|
||||||
|
getDropdownFocusIdForRecordField(
|
||||||
|
recordId,
|
||||||
|
fieldDefinition.fieldMetadataId,
|
||||||
|
'table-cell',
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
getClickOutsideListenerIsActivatedState,
|
getClickOutsideListenerIsActivatedState,
|
||||||
@ -156,6 +169,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
|||||||
setViewableRecordNameSingular,
|
setViewableRecordNameSingular,
|
||||||
openRightDrawer,
|
openRightDrawer,
|
||||||
setHotkeyScope,
|
setHotkeyScope,
|
||||||
|
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
|
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
|
||||||
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
|
||||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useTableColumns } from '../../hooks/useTableColumns';
|
import { useTableColumns } from '../../hooks/useTableColumns';
|
||||||
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
import { ColumnDefinition } from '../../types/ColumnDefinition';
|
||||||
@ -92,45 +91,43 @@ export const RecordTableColumnHeadDropdownMenu = ({
|
|||||||
const canHide = column.isLabelIdentifier !== true;
|
const canHide = column.isLabelIdentifier !== true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<DropdownMenuItemsContainer>
|
||||||
<DropdownMenuItemsContainer>
|
{isFilterable && (
|
||||||
{isFilterable && (
|
<MenuItem
|
||||||
<MenuItem
|
LeftIcon={IconFilter}
|
||||||
LeftIcon={IconFilter}
|
onClick={handleFilterClick}
|
||||||
onClick={handleFilterClick}
|
text="Filter"
|
||||||
text="Filter"
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{isSortable && (
|
||||||
{isSortable && (
|
<MenuItem
|
||||||
<MenuItem
|
LeftIcon={IconSortDescending}
|
||||||
LeftIcon={IconSortDescending}
|
onClick={handleSortClick}
|
||||||
onClick={handleSortClick}
|
text="Sort"
|
||||||
text="Sort"
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{showSeparator && <DropdownMenuSeparator />}
|
||||||
{showSeparator && <DropdownMenuSeparator />}
|
{canMoveLeft && (
|
||||||
{canMoveLeft && (
|
<MenuItem
|
||||||
<MenuItem
|
LeftIcon={IconArrowLeft}
|
||||||
LeftIcon={IconArrowLeft}
|
onClick={handleColumnMoveLeft}
|
||||||
onClick={handleColumnMoveLeft}
|
text="Move left"
|
||||||
text="Move left"
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{canMoveRight && (
|
||||||
{canMoveRight && (
|
<MenuItem
|
||||||
<MenuItem
|
LeftIcon={IconArrowRight}
|
||||||
LeftIcon={IconArrowRight}
|
onClick={handleColumnMoveRight}
|
||||||
onClick={handleColumnMoveRight}
|
text="Move right"
|
||||||
text="Move right"
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{canHide && (
|
||||||
{canHide && (
|
<MenuItem
|
||||||
<MenuItem
|
LeftIcon={IconEyeOff}
|
||||||
LeftIcon={IconEyeOff}
|
onClick={handleColumnVisibility}
|
||||||
onClick={handleColumnVisibility}
|
text="Hide"
|
||||||
text="Hide"
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</ScrollWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
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';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
export const RecordTableHeaderPlusButtonContent = () => {
|
export const RecordTableHeaderPlusButtonContent = () => {
|
||||||
@ -42,20 +41,18 @@ export const RecordTableHeaderPlusButtonContent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
|
||||||
<DropdownMenuItemsContainer>
|
|
||||||
{hiddenTableColumns.map((column) => (
|
|
||||||
<MenuItem
|
|
||||||
key={column.fieldMetadataId}
|
|
||||||
onClick={() => handleAddColumn(column)}
|
|
||||||
LeftIcon={getIcon(column.iconName)}
|
|
||||||
text={column.label}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</ScrollWrapper>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
|
{hiddenTableColumns.map((column) => (
|
||||||
|
<MenuItem
|
||||||
|
key={column.fieldMetadataId}
|
||||||
|
onClick={() => handleAddColumn(column)}
|
||||||
|
LeftIcon={getIcon(column.iconName)}
|
||||||
|
text={column.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||||
<UndecoratedLink
|
<UndecoratedLink
|
||||||
fullWidth
|
fullWidth
|
||||||
to={`/settings/objects/${getObjectSlug(objectMetadataItem)}`}
|
to={`/settings/objects/${getObjectSlug(objectMetadataItem)}`}
|
||||||
|
|||||||
@ -179,7 +179,11 @@ export const MultiRecordSelect = ({
|
|||||||
{objectRecordsIdsMultiSelect?.length > 0 && (
|
{objectRecordsIdsMultiSelect?.length > 0 && (
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
)}
|
)}
|
||||||
{isDefined(onCreate) && <div>{createNewButton}</div>}
|
{isDefined(onCreate) && (
|
||||||
|
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||||
|
{createNewButton}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
|||||||
import { MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordClickOutsideListenerId';
|
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 { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
export const MultipleObjectRecordOnClickOutsideEffect = ({
|
export const MultipleObjectRecordOnClickOutsideEffect = ({
|
||||||
containerRef,
|
containerRef,
|
||||||
@ -11,10 +12,6 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({
|
|||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
onClickOutside: () => void;
|
onClickOutside: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { useListenClickOutside } = useClickOutsideListener(
|
|
||||||
MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
|
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
|
||||||
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
|
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
|
||||||
|
|
||||||
@ -35,6 +32,7 @@ export const MultipleObjectRecordOnClickOutsideEffect = ({
|
|||||||
|
|
||||||
onClickOutside();
|
onClickOutside();
|
||||||
},
|
},
|
||||||
|
listenerId: MULTI_OBJECT_RECORD_CLICK_OUTSIDE_LISTENER_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export const SingleRecordSelect = ({
|
|||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
listenerId: 'single-record-select',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -59,27 +59,6 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = (
|
|
||||||
<SingleRecordSelectMenuItems
|
|
||||||
recordsToSelect={records.recordsToSelect}
|
|
||||||
loading={records.loading}
|
|
||||||
selectedRecord={
|
|
||||||
records.recordsToSelect.length === 1
|
|
||||||
? records.recordsToSelect[0]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
|
||||||
hotkeyScope={recordPickerInstanceId}
|
|
||||||
isFiltered={!!recordPickerSearchFilter}
|
|
||||||
{...{
|
|
||||||
EmptyIcon,
|
|
||||||
emptyLabel,
|
|
||||||
onCancel,
|
|
||||||
onRecordSelected,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{dropdownPlacement?.includes('end') && (
|
{dropdownPlacement?.includes('end') && (
|
||||||
@ -88,7 +67,26 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
|||||||
{createNewButton}
|
{createNewButton}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||||
{records.recordsToSelect.length > 0 && results}
|
{records.recordsToSelect.length > 0 && (
|
||||||
|
<SingleRecordSelectMenuItems
|
||||||
|
recordsToSelect={records.recordsToSelect}
|
||||||
|
loading={records.loading}
|
||||||
|
selectedRecord={
|
||||||
|
records.recordsToSelect.length === 1
|
||||||
|
? records.recordsToSelect[0]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
||||||
|
hotkeyScope={recordPickerInstanceId}
|
||||||
|
isFiltered={!!recordPickerSearchFilter}
|
||||||
|
{...{
|
||||||
|
EmptyIcon,
|
||||||
|
emptyLabel,
|
||||||
|
onCancel,
|
||||||
|
onRecordSelected,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -97,7 +95,26 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
|||||||
isUndefinedOrNull(dropdownPlacement)) && (
|
isUndefinedOrNull(dropdownPlacement)) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{records.recordsToSelect.length > 0 && results}
|
{records.recordsToSelect.length > 0 && (
|
||||||
|
<SingleRecordSelectMenuItems
|
||||||
|
recordsToSelect={records.recordsToSelect}
|
||||||
|
loading={records.loading}
|
||||||
|
selectedRecord={
|
||||||
|
records.recordsToSelect.length === 1
|
||||||
|
? records.recordsToSelect[0]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
||||||
|
hotkeyScope={recordPickerInstanceId}
|
||||||
|
isFiltered={!!recordPickerSearchFilter}
|
||||||
|
{...{
|
||||||
|
EmptyIcon,
|
||||||
|
emptyLabel,
|
||||||
|
onCancel,
|
||||||
|
onRecordSelected,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
export const getDropdownFocusIdForRecordField = (
|
||||||
|
recordId: string,
|
||||||
|
fieldMetadataId: string,
|
||||||
|
componentType: 'table-cell' | 'inline-cell',
|
||||||
|
) => {
|
||||||
|
return `dropdown-${componentType}-record-${recordId}-field-${fieldMetadataId}`;
|
||||||
|
};
|
||||||
@ -19,12 +19,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
|
|
||||||
type SettingsAccountsRowDropdownMenuProps = {
|
type SettingsAccountsRowDropdownMenuProps = {
|
||||||
account: ConnectedAccount;
|
account: ConnectedAccount;
|
||||||
className?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsAccountsRowDropdownMenu = ({
|
export const SettingsAccountsRowDropdownMenu = ({
|
||||||
account,
|
account,
|
||||||
className,
|
|
||||||
}: SettingsAccountsRowDropdownMenuProps) => {
|
}: SettingsAccountsRowDropdownMenuProps) => {
|
||||||
const dropdownId = `settings-account-row-${account.id}`;
|
const dropdownId = `settings-account-row-${account.id}`;
|
||||||
|
|
||||||
@ -39,7 +37,6 @@ export const SettingsAccountsRowDropdownMenu = ({
|
|||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownId}
|
dropdownId={dropdownId}
|
||||||
className={className}
|
|
||||||
dropdownPlacement="right-start"
|
dropdownPlacement="right-start"
|
||||||
dropdownHotkeyScope={{ scope: dropdownId }}
|
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
|
|||||||
@ -101,6 +101,7 @@ export const MatchColumnSelect = ({
|
|||||||
callback: () => {
|
callback: () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
},
|
},
|
||||||
|
listenerId: 'match-column-select',
|
||||||
});
|
});
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
|
|||||||
@ -75,8 +75,6 @@ export const SubMatchingSelect = <T extends string>({
|
|||||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||||
const value = options.find((opt) => opt.value === option.value);
|
const value = options.find((opt) => opt.value === option.value);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selectWrapperRef, setSelectWrapperRef] =
|
|
||||||
useState<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -106,17 +104,14 @@ export const SubMatchingSelect = <T extends string>({
|
|||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
id="control"
|
id="control"
|
||||||
ref={setSelectWrapperRef}
|
|
||||||
>
|
>
|
||||||
<Tag
|
<Tag
|
||||||
text={value?.label ?? placeholder}
|
text={value?.label ?? placeholder}
|
||||||
color={value?.color as TagColor}
|
color={value?.color as TagColor}
|
||||||
/>
|
/>
|
||||||
<StyledIconChevronDown size={theme.icon.size.md} />
|
<StyledIconChevronDown size={theme.icon.size.md} />
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
parentRef={selectWrapperRef}
|
|
||||||
defaultOption={value}
|
defaultOption={value}
|
||||||
options={options}
|
options={options}
|
||||||
onOptionSelected={handleSelect}
|
onOptionSelected={handleSelect}
|
||||||
|
|||||||
@ -8,10 +8,11 @@ import { CountrySelect } from '@/ui/input/components/internal/country/components
|
|||||||
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
|
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
|
||||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined, MOBILE_VIEWPORT } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledAddressContainer = styled.div`
|
const StyledAddressContainer = styled.div`
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
@ -190,18 +191,22 @@ export const AddressInput = ({
|
|||||||
[onEscape, internalValue],
|
[onEscape, internalValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { useListenClickOutside } = useClickOutsideListener('addressInput');
|
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [wrapperRef],
|
refs: [wrapperRef],
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
|
if (activeDropdownFocusId === SELECT_COUNTRY_DROPDOWN_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
closeCountryDropdown();
|
closeCountryDropdown();
|
||||||
|
|
||||||
onClickOutside?.(event, internalValue);
|
onClickOutside?.(event, internalValue);
|
||||||
},
|
},
|
||||||
enabled: isDefined(onClickOutside),
|
enabled: isDefined(onClickOutside),
|
||||||
|
listenerId: 'address-input',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||||
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
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`
|
const StyledCalendarContainer = styled.div`
|
||||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
@ -72,7 +72,7 @@ export const DateInput = ({
|
|||||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [wrapperRef],
|
refs: [wrapperRef],
|
||||||
listenerId: 'DateInput',
|
listenerId: 'DateInput',
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
|
|||||||
@ -10,9 +10,9 @@ import { Key } from 'ts-key-enum';
|
|||||||
|
|
||||||
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
|
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { splitFullName } from '~/utils/format/spiltFullName';
|
import { splitFullName } from '~/utils/format/spiltFullName';
|
||||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||||
import { StyledTextInput } from './TextInput';
|
import { StyledTextInput } from './TextInput';
|
||||||
@ -158,6 +158,7 @@ export const DoubleTextInput = ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabled: isDefined(onClickOutside),
|
enabled: isDefined(onClickOutside),
|
||||||
|
listenerId: 'double-text-input',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOnPaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
const handleOnPaste = (event: ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import styled from '@emotion/styled';
|
|||||||
import { OVERLAY_BACKGROUND } from 'twenty-ui';
|
import { OVERLAY_BACKGROUND } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledFieldTextAreaOverlay = styled.div`
|
const StyledFieldTextAreaOverlay = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
margin: -1px;
|
||||||
|
max-height: 420px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
max-height: 420px;
|
|
||||||
margin: -1px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
${OVERLAY_BACKGROUND}
|
${OVERLAY_BACKGROUND}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import { SelectOption } from '@/spreadsheet-import/types';
|
import { SelectOption } from '@/spreadsheet-import/types';
|
||||||
import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput';
|
import { SelectInput as SelectBaseInput } from '@/ui/input/components/SelectInput';
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { ReferenceType } from '@floating-ui/react';
|
|
||||||
|
|
||||||
type SelectInputProps = {
|
type SelectInputProps = {
|
||||||
selectableListId: string;
|
selectableListId: string;
|
||||||
selectableItemIdArray: string[];
|
selectableItemIdArray: string[];
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
onEnter: (itemId: string) => void;
|
onEnter: (itemId: string) => void;
|
||||||
selectWrapperRef?: ReferenceType | null | undefined;
|
|
||||||
onOptionSelected: (selectedOption: SelectOption) => void;
|
onOptionSelected: (selectedOption: SelectOption) => void;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
@ -23,7 +21,6 @@ export const SelectInput = ({
|
|||||||
selectableItemIdArray,
|
selectableItemIdArray,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
onEnter,
|
onEnter,
|
||||||
selectWrapperRef,
|
|
||||||
onOptionSelected,
|
onOptionSelected,
|
||||||
options,
|
options,
|
||||||
onCancel,
|
onCancel,
|
||||||
@ -40,7 +37,6 @@ export const SelectInput = ({
|
|||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
>
|
>
|
||||||
<SelectBaseInput
|
<SelectBaseInput
|
||||||
parentRef={selectWrapperRef}
|
|
||||||
onOptionSelected={onOptionSelected}
|
onOptionSelected={onOptionSelected}
|
||||||
options={options}
|
options={options}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
|||||||
@ -168,7 +168,7 @@ export const Select = <Value extends SelectValue>({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
)}
|
)}
|
||||||
{!!callToActionButton && (
|
{!!callToActionButton && (
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight withoutScrollWrapper>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={callToActionButton.onClick}
|
onClick={callToActionButton.onClick}
|
||||||
LeftIcon={callToActionButton.Icon}
|
LeftIcon={callToActionButton.Icon}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { SelectOption } from '@/spreadsheet-import/types';
|
import { SelectOption } from '@/spreadsheet-import/types';
|
||||||
|
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
@ -8,32 +6,15 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import {
|
|
||||||
ReferenceType,
|
|
||||||
autoUpdate,
|
|
||||||
flip,
|
|
||||||
offset,
|
|
||||||
size,
|
|
||||||
useFloating,
|
|
||||||
} from '@floating-ui/react';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { MenuItemSelectTag, TagColor, isDefined } from 'twenty-ui';
|
import { MenuItemSelectTag, TagColor, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledRelationPickerContainer = styled.div`
|
|
||||||
left: -1px;
|
|
||||||
position: absolute;
|
|
||||||
top: -1px;
|
|
||||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface SelectInputProps {
|
interface SelectInputProps {
|
||||||
onOptionSelected: (selectedOption: SelectOption) => void;
|
onOptionSelected: (selectedOption: SelectOption) => void;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
defaultOption?: SelectOption;
|
defaultOption?: SelectOption;
|
||||||
parentRef?: ReferenceType | null | undefined;
|
|
||||||
onFilterChange?: (filteredOptions: SelectOption[]) => void;
|
onFilterChange?: (filteredOptions: SelectOption[]) => void;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
clearLabel?: string;
|
clearLabel?: string;
|
||||||
@ -47,13 +28,11 @@ export const SelectInput = ({
|
|||||||
options,
|
options,
|
||||||
onCancel,
|
onCancel,
|
||||||
defaultOption,
|
defaultOption,
|
||||||
parentRef,
|
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
}: SelectInputProps) => {
|
}: SelectInputProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
const [selectedOption, setSelectedOption] = useState<
|
const [selectedOption, setSelectedOption] = useState<
|
||||||
SelectOption | undefined
|
SelectOption | undefined
|
||||||
@ -81,27 +60,12 @@ export const SelectInput = ({
|
|||||||
onOptionSelected(option);
|
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(() => {
|
useEffect(() => {
|
||||||
onFilterChange?.(optionsInDropDown);
|
onFilterChange?.(optionsInDropDown);
|
||||||
}, [onFilterChange, optionsInDropDown]);
|
}, [onFilterChange, optionsInDropDown]);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [refs.floating],
|
refs: [containerRef],
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
@ -113,6 +77,7 @@ export const SelectInput = ({
|
|||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
listenerId: 'select-input',
|
||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
@ -130,44 +95,39 @@ export const SelectInput = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRelationPickerContainer
|
<DropdownMenu ref={containerRef} data-select-disable>
|
||||||
ref={refs.setFloating}
|
<DropdownMenuSearchInput
|
||||||
style={floatingStyles}
|
value={searchFilter}
|
||||||
>
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
<DropdownMenu ref={containerRef} data-select-disable>
|
autoFocus
|
||||||
<DropdownMenuSearchInput
|
/>
|
||||||
value={searchFilter}
|
<DropdownMenuSeparator />
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
autoFocus
|
{onClear && clearLabel && (
|
||||||
/>
|
<MenuItemSelectTag
|
||||||
<DropdownMenuSeparator />
|
key={`No ${clearLabel}`}
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
selected={false}
|
||||||
{onClear && clearLabel && (
|
text={`No ${clearLabel}`}
|
||||||
|
color="transparent"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOption(undefined);
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{optionsInDropDown.map((option) => {
|
||||||
|
return (
|
||||||
<MenuItemSelectTag
|
<MenuItemSelectTag
|
||||||
key={`No ${clearLabel}`}
|
key={option.value}
|
||||||
selected={false}
|
selected={selectedOption?.value === option.value}
|
||||||
text={`No ${clearLabel}`}
|
text={option.label}
|
||||||
color="transparent"
|
color={option.color as TagColor}
|
||||||
variant="outline"
|
onClick={() => handleOptionChange(option)}
|
||||||
onClick={() => {
|
|
||||||
setSelectedOption(undefined);
|
|
||||||
onClear();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
{optionsInDropDown.map((option) => {
|
})}
|
||||||
return (
|
</DropdownMenuItemsContainer>
|
||||||
<MenuItemSelectTag
|
</DropdownMenu>
|
||||||
key={option.value}
|
|
||||||
selected={selectedOption?.value === option.value}
|
|
||||||
text={option.label}
|
|
||||||
color={option.color as TagColor}
|
|
||||||
onClick={() => handleOptionChange(option)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</DropdownMenu>
|
|
||||||
</StyledRelationPickerContainer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,11 +10,7 @@ import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope';
|
|||||||
|
|
||||||
import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect';
|
import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect';
|
||||||
|
|
||||||
type StyledDropdownButtonProps = {
|
const StyledDropdownButtonContainer = styled.div`
|
||||||
isUnfolded: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ color }) => color ?? 'none'};
|
color: ${({ color }) => color ?? 'none'};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -62,7 +58,7 @@ export const CurrencyPickerDropdownButton = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { isDropdownOpen, closeDropdown } = useDropdown(
|
const { closeDropdown } = useDropdown(
|
||||||
CurrencyPickerHotkeyScope.CurrencyPicker,
|
CurrencyPickerHotkeyScope.CurrencyPicker,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -77,11 +73,10 @@ export const CurrencyPickerDropdownButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownMenuWidth={200}
|
dropdownId="currency-picker-dropdown-id"
|
||||||
dropdownId="currncy-picker-dropdown-id"
|
|
||||||
dropdownHotkeyScope={{ scope: CurrencyPickerHotkeyScope.CurrencyPicker }}
|
dropdownHotkeyScope={{ scope: CurrencyPickerHotkeyScope.CurrencyPicker }}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
<StyledDropdownButtonContainer>
|
||||||
<StyledIconContainer>
|
<StyledIconContainer>
|
||||||
{currencyCode}
|
{currencyCode}
|
||||||
<IconChevronDown size={theme.icon.size.sm} />
|
<IconChevronDown size={theme.icon.size.sm} />
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const CurrencyPickerDropdownSelect = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu width="240px" disableBlur>
|
<DropdownMenu disableBlur>
|
||||||
<DropdownMenuSearchInput
|
<DropdownMenuSearchInput
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(event) => setSearchFilter(event.target.value)}
|
onChange={(event) => setSearchFilter(event.target.value)}
|
||||||
|
|||||||
@ -1,32 +1,26 @@
|
|||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
flip,
|
flip,
|
||||||
FloatingPortal,
|
|
||||||
offset,
|
offset,
|
||||||
Placement,
|
Placement,
|
||||||
size,
|
size,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/react';
|
} from '@floating-ui/react';
|
||||||
import { MouseEvent, ReactNode, useEffect, useRef } from 'react';
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
import { Keys } from 'react-hotkeys-hook';
|
import { Keys } from 'react-hotkeys-hook';
|
||||||
import { Key } from 'ts-key-enum';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
import { useDropdown } from '../hooks/useDropdown';
|
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 { DropdownUnmountEffect } from '@/ui/layout/dropdown/components/DropdownUnmountEffect';
|
||||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
|
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
|
||||||
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
|
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
|
||||||
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { flushSync } from 'react-dom';
|
||||||
import { DropdownMenu } from './DropdownMenu';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
|
import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
|
||||||
|
|
||||||
type DropdownProps = {
|
type DropdownProps = {
|
||||||
@ -62,24 +56,15 @@ export const Dropdown = ({
|
|||||||
dropdownStrategy = 'absolute',
|
dropdownStrategy = 'absolute',
|
||||||
dropdownOffset = { x: 0, y: 0 },
|
dropdownOffset = { x: 0, y: 0 },
|
||||||
disableBlur = false,
|
disableBlur = false,
|
||||||
usePortal = false,
|
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
onClose,
|
onClose,
|
||||||
onOpen,
|
onOpen,
|
||||||
}: DropdownProps) => {
|
}: DropdownProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
const {
|
|
||||||
isDropdownOpen,
|
|
||||||
toggleDropdown,
|
|
||||||
closeDropdown,
|
|
||||||
dropdownWidth,
|
|
||||||
setDropdownPlacement,
|
|
||||||
} = useDropdown(dropdownId);
|
|
||||||
|
|
||||||
const offsetMiddlewares = [];
|
const offsetMiddlewares = [];
|
||||||
|
|
||||||
const [dropdownMaxHeight, setDropdownMaxHeight] = useRecoilComponentStateV2(
|
const setDropdownMaxHeight = useSetRecoilComponentStateV2(
|
||||||
dropdownMaxHeightComponentStateV2,
|
dropdownMaxHeightComponentStateV2,
|
||||||
dropdownId,
|
dropdownId,
|
||||||
);
|
);
|
||||||
@ -111,14 +96,6 @@ export const Dropdown = ({
|
|||||||
strategy: dropdownStrategy,
|
strategy: dropdownStrategy,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDropdownPlacement(placement);
|
|
||||||
}, [placement, setDropdownPlacement]);
|
|
||||||
|
|
||||||
const handleHotkeyTriggered = () => {
|
|
||||||
toggleDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickableComponentClick = (event: MouseEvent) => {
|
const handleClickableComponentClick = (event: MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -127,88 +104,41 @@ export const Dropdown = ({
|
|||||||
onClickOutside?.();
|
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 (
|
return (
|
||||||
<DropdownComponentInstanceContext.Provider
|
<DropdownComponentInstanceContext.Provider
|
||||||
value={{ instanceId: dropdownId }}
|
value={{ instanceId: dropdownId }}
|
||||||
>
|
>
|
||||||
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
|
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
|
||||||
<div ref={containerRef} className={className}>
|
<>
|
||||||
{clickableComponent && (
|
{clickableComponent && (
|
||||||
<div
|
<div
|
||||||
ref={refs.setReference}
|
ref={refs.setReference}
|
||||||
onClick={handleClickableComponentClick}
|
onClick={handleClickableComponentClick}
|
||||||
className={className}
|
|
||||||
>
|
>
|
||||||
{clickableComponent}
|
{clickableComponent}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hotkey && (
|
{isDropdownOpen && (
|
||||||
<HotkeyEffect
|
<DropdownContent
|
||||||
hotkey={hotkey}
|
className={className}
|
||||||
onHotkeyTriggered={handleHotkeyTriggered}
|
floatingStyles={floatingStyles}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isDropdownOpen && usePortal && (
|
|
||||||
<FloatingPortal>
|
|
||||||
<DropdownMenu
|
|
||||||
disableBlur={disableBlur}
|
|
||||||
width={dropdownMenuWidth ?? dropdownWidth}
|
|
||||||
data-select-disable
|
|
||||||
ref={refs.setFloating}
|
|
||||||
style={dropdownMenuStyles}
|
|
||||||
>
|
|
||||||
{dropdownComponents}
|
|
||||||
</DropdownMenu>
|
|
||||||
</FloatingPortal>
|
|
||||||
)}
|
|
||||||
{isDropdownOpen && !usePortal && (
|
|
||||||
<DropdownMenu
|
|
||||||
disableBlur={disableBlur}
|
disableBlur={disableBlur}
|
||||||
width={dropdownMenuWidth ?? dropdownWidth}
|
dropdownMenuWidth={dropdownMenuWidth}
|
||||||
data-select-disable
|
dropdownComponents={dropdownComponents}
|
||||||
ref={refs.setFloating}
|
dropdownId={dropdownId}
|
||||||
style={dropdownMenuStyles}
|
dropdownPlacement={placement ?? 'bottom-end'}
|
||||||
>
|
floatingUiRefs={refs}
|
||||||
{dropdownComponents}
|
hotkeyScope={dropdownHotkeyScope}
|
||||||
</DropdownMenu>
|
hotkey={hotkey}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onHotkeyTriggered={toggleDropdown}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<DropdownOnToggleEffect
|
<DropdownOnToggleEffect
|
||||||
onDropdownClose={onClose}
|
onDropdownClose={onClose}
|
||||||
onDropdownOpen={onOpen}
|
onDropdownOpen={onOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
</DropdownScope>
|
</DropdownScope>
|
||||||
<DropdownUnmountEffect dropdownId={dropdownId} />
|
<DropdownUnmountEffect dropdownId={dropdownId} />
|
||||||
</DropdownComponentInstanceContext.Provider>
|
</DropdownComponentInstanceContext.Provider>
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
<HotkeyEffect hotkey={hotkey} onHotkeyTriggered={onHotkeyTriggered} />
|
||||||
|
)}
|
||||||
|
<FloatingPortal>
|
||||||
|
<DropdownMenu
|
||||||
|
className={className}
|
||||||
|
disableBlur={disableBlur}
|
||||||
|
width={dropdownMenuWidth ?? dropdownWidth}
|
||||||
|
data-select-disable
|
||||||
|
ref={floatingUiRefs.setFloating}
|
||||||
|
style={dropdownMenuStyles}
|
||||||
|
>
|
||||||
|
{dropdownComponents}
|
||||||
|
</DropdownMenu>
|
||||||
|
</FloatingPortal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -18,7 +18,6 @@ const StyledHeader = styled.li`
|
|||||||
padding: ${({ theme }) => theme.spacing(1)};
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: inherit;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${({ theme, onClick }) =>
|
background: ${({ theme, onClick }) =>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const StyledInputContainer = styled.div`
|
|||||||
|
|
||||||
const StyledRightContainer = styled.div`
|
const StyledRightContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: ${({ theme }) => theme.spacing(1)};
|
right: ${({ theme }) => theme.spacing(2)};
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -37,12 +37,14 @@ export const DropdownMenuItemsContainer = ({
|
|||||||
children,
|
children,
|
||||||
hasMaxHeight,
|
hasMaxHeight,
|
||||||
className,
|
className,
|
||||||
|
withoutScrollWrapper,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
hasMaxHeight?: boolean;
|
hasMaxHeight?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
withoutScrollWrapper?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return withoutScrollWrapper === true ? (
|
||||||
<StyledDropdownMenuItemsExternalContainer
|
<StyledDropdownMenuItemsExternalContainer
|
||||||
hasMaxHeight={hasMaxHeight}
|
hasMaxHeight={hasMaxHeight}
|
||||||
className={className}
|
className={className}
|
||||||
@ -59,5 +61,16 @@ export const DropdownMenuItemsContainer = ({
|
|||||||
</StyledDropdownMenuItemsInternalContainer>
|
</StyledDropdownMenuItemsInternalContainer>
|
||||||
)}
|
)}
|
||||||
</StyledDropdownMenuItemsExternalContainer>
|
</StyledDropdownMenuItemsExternalContainer>
|
||||||
|
) : (
|
||||||
|
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
||||||
|
<StyledDropdownMenuItemsExternalContainer
|
||||||
|
hasMaxHeight={hasMaxHeight}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<StyledDropdownMenuItemsInternalContainer>
|
||||||
|
{children}
|
||||||
|
</StyledDropdownMenuItemsInternalContainer>
|
||||||
|
</StyledDropdownMenuItemsExternalContainer>
|
||||||
|
</ScrollWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates';
|
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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@ -17,6 +19,12 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
|
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||||
|
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||||
|
|
||||||
|
const { goBackToPreviousDropdownFocusId } =
|
||||||
|
useGoBackToPreviousDropdownFocusId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
@ -34,17 +42,28 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
useRecoilState(isDropdownOpenState);
|
useRecoilState(isDropdownOpenState);
|
||||||
|
|
||||||
const closeDropdown = useCallback(() => {
|
const closeDropdown = useCallback(() => {
|
||||||
goBackToPreviousHotkeyScope();
|
if (isDropdownOpen) {
|
||||||
setIsDropdownOpen(false);
|
goBackToPreviousHotkeyScope();
|
||||||
}, [goBackToPreviousHotkeyScope, setIsDropdownOpen]);
|
setIsDropdownOpen(false);
|
||||||
|
goBackToPreviousDropdownFocusId();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isDropdownOpen,
|
||||||
|
goBackToPreviousHotkeyScope,
|
||||||
|
setIsDropdownOpen,
|
||||||
|
goBackToPreviousDropdownFocusId,
|
||||||
|
]);
|
||||||
|
|
||||||
const openDropdown = () => {
|
const openDropdown = () => {
|
||||||
setIsDropdownOpen(true);
|
if (!isDropdownOpen) {
|
||||||
if (isDefined(dropdownHotkeyScope)) {
|
setIsDropdownOpen(true);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId ?? scopeId);
|
||||||
dropdownHotkeyScope.scope,
|
if (isDefined(dropdownHotkeyScope)) {
|
||||||
dropdownHotkeyScope.customScopes,
|
setHotkeyScopeAndMemorizePreviousScope(
|
||||||
);
|
dropdownHotkeyScope.scope,
|
||||||
|
dropdownHotkeyScope.customScopes,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const activeDropdownFocusIdState = createState<string | null>({
|
||||||
|
key: 'activeDropdownFocusIdState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const previousDropdownFocusIdState = createState<string | null>({
|
||||||
|
key: 'previousDropdownFocusIdState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@ -4,7 +4,6 @@ import { AnimatedContainer, Chip, ChipVariant } from 'twenty-ui';
|
|||||||
|
|
||||||
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown';
|
||||||
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -101,20 +100,18 @@ export const ExpandableList = ({
|
|||||||
resetFirstHiddenChildIndex();
|
resetFirstHiddenChildIndex();
|
||||||
}, [isChipCountDisplayed, children.length, resetFirstHiddenChildIndex]);
|
}, [isChipCountDisplayed, children.length, resetFirstHiddenChildIndex]);
|
||||||
|
|
||||||
useListenClickOutside({
|
const handleClickOutside = () => {
|
||||||
refs: [containerRef],
|
setIsListExpanded(false);
|
||||||
callback: () => {
|
|
||||||
// Handle container resize
|
if (
|
||||||
if (
|
childrenContainerElement?.clientWidth !== previousChildrenContainerWidth
|
||||||
childrenContainerElement?.clientWidth !== previousChildrenContainerWidth
|
) {
|
||||||
) {
|
resetFirstHiddenChildIndex();
|
||||||
resetFirstHiddenChildIndex();
|
setPreviousChildrenContainerWidth(
|
||||||
setPreviousChildrenContainerWidth(
|
childrenContainerElement?.clientWidth ?? 0,
|
||||||
childrenContainerElement?.clientWidth ?? 0,
|
);
|
||||||
);
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
@ -163,10 +160,7 @@ export const ExpandableList = ({
|
|||||||
{isListExpanded && (
|
{isListExpanded && (
|
||||||
<ExpandedListDropdown
|
<ExpandedListDropdown
|
||||||
anchorElement={containerRef.current ?? undefined}
|
anchorElement={containerRef.current ?? undefined}
|
||||||
onClickOutside={() => {
|
onClickOutside={handleClickOutside}
|
||||||
resetFirstHiddenChildIndex();
|
|
||||||
setIsListExpanded(false);
|
|
||||||
}}
|
|
||||||
withBorder={withExpandedListBorder}
|
withBorder={withExpandedListBorder}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -46,8 +46,11 @@ export const ExpandedListDropdown = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [refs.floating],
|
refs: [refs.domReference],
|
||||||
callback: onClickOutside ?? (() => {}),
|
callback: () => {
|
||||||
|
onClickOutside?.();
|
||||||
|
},
|
||||||
|
listenerId: 'expandable-list',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
|
|||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import {
|
import {
|
||||||
ClickOutsideMode,
|
ClickOutsideMode,
|
||||||
useListenClickOutsideV2,
|
useListenClickOutside,
|
||||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@ -207,7 +207,7 @@ export const Modal = ({
|
|||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
);
|
);
|
||||||
|
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [modalRef],
|
refs: [modalRef],
|
||||||
listenerId: 'MODAL_CLICK_OUTSIDE_LISTENER_ID',
|
listenerId: 'MODAL_CLICK_OUTSIDE_LISTENER_ID',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
|||||||
@ -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 { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState';
|
||||||
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
|
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useRef } from 'react';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { Key } from 'ts-key-enum';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useRightDrawer } from '../hooks/useRightDrawer';
|
|
||||||
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
||||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
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 { RightDrawerRouter } from './RightDrawerRouter';
|
||||||
import { workflowReactFlowRefState } from '@/workflow/states/workflowReactFlowRefState';
|
|
||||||
|
|
||||||
const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>`
|
const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>`
|
||||||
background: ${({ theme }) => theme.background.primary};
|
background: ${({ theme }) => theme.background.primary};
|
||||||
@ -40,7 +33,7 @@ const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>`
|
|||||||
|
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 30;
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
background: ${({ theme }) => theme.background.overlayTertiary};
|
background: ${({ theme }) => theme.background.overlayTertiary};
|
||||||
@ -56,39 +49,6 @@ const StyledRightDrawer = styled.div`
|
|||||||
export const RightDrawer = () => {
|
export const RightDrawer = () => {
|
||||||
const theme = useTheme();
|
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 isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
|
||||||
|
|
||||||
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
|
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
|
||||||
@ -99,52 +59,6 @@ export const RightDrawer = () => {
|
|||||||
|
|
||||||
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
|
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
|
||||||
|
|
||||||
const { closeRightDrawer } = useRightDrawer();
|
|
||||||
|
|
||||||
const rightDrawerRef = useRef<HTMLDivElement>(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 isMobile = useIsMobile();
|
||||||
|
|
||||||
const targetVariantForAnimation: RightDrawerAnimationVariant =
|
const targetVariantForAnimation: RightDrawerAnimationVariant =
|
||||||
@ -168,13 +82,13 @@ export const RightDrawer = () => {
|
|||||||
<StyledContainer
|
<StyledContainer
|
||||||
isRightDrawerMinimized={isRightDrawerMinimized}
|
isRightDrawerMinimized={isRightDrawerMinimized}
|
||||||
animate={targetVariantForAnimation}
|
animate={targetVariantForAnimation}
|
||||||
variants={animationVariants}
|
variants={RIGHT_DRAWER_ANIMATION_VARIANTS}
|
||||||
transition={{
|
transition={{
|
||||||
duration: theme.animation.duration.normal,
|
duration: theme.animation.duration.normal,
|
||||||
}}
|
}}
|
||||||
onAnimationComplete={handleAnimationComplete}
|
onAnimationComplete={handleAnimationComplete}
|
||||||
>
|
>
|
||||||
<StyledRightDrawer ref={rightDrawerRef}>
|
<StyledRightDrawer>
|
||||||
{isRightDrawerOpen && <RightDrawerRouter />}
|
{isRightDrawerOpen && <RightDrawerRouter />}
|
||||||
</StyledRightDrawer>
|
</StyledRightDrawer>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@ -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<HTMLDivElement>(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 (
|
||||||
|
<StyledRightDrawerPage ref={rightDrawerRef}>
|
||||||
|
{children}
|
||||||
|
</StyledRightDrawerPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,22 +7,16 @@ import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/compone
|
|||||||
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
|
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
|
||||||
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
|
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 { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
|
||||||
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
||||||
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
|
import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep';
|
||||||
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
|
import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction';
|
||||||
|
import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/components/RightDrawerWorkflowSelectTriggerType';
|
||||||
import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep';
|
import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||||
import { RightDrawerPages } from '../types/RightDrawerPages';
|
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`
|
const StyledRightDrawerBody = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -61,13 +55,13 @@ export const RightDrawerRouter = () => {
|
|||||||
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
|
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRightDrawerPage>
|
<RightDrawerContainer>
|
||||||
<RightDrawerTopBar />
|
<RightDrawerTopBar />
|
||||||
{!isRightDrawerMinimized && (
|
{!isRightDrawerMinimized && (
|
||||||
<StyledRightDrawerBody>
|
<StyledRightDrawerBody>
|
||||||
{rightDrawerPageComponent}
|
{rightDrawerPageComponent}
|
||||||
</StyledRightDrawerBody>
|
</StyledRightDrawerBody>
|
||||||
)}
|
)}
|
||||||
</StyledRightDrawerPage>
|
</RightDrawerContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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;
|
||||||
@ -96,6 +96,7 @@ export const NavigationDrawerInput = ({
|
|||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
onClickOutside(event, value);
|
onClickOutside(event, value);
|
||||||
},
|
},
|
||||||
|
listenerId: 'navigation-drawer-input',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
|
import { fireEvent, renderHook } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { fireEvent, renderHook } from '@testing-library/react';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClickOutsideMode,
|
ClickOutsideMode,
|
||||||
useListenClickOutsideV2,
|
useListenClickOutside,
|
||||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const containerRef = React.createRef<HTMLDivElement>();
|
const containerRef = React.createRef<HTMLDivElement>();
|
||||||
@ -19,13 +19,13 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const listenerId = 'listenerId';
|
const listenerId = 'listenerId';
|
||||||
describe('useListenClickOutsideV2', () => {
|
describe('useListenClickOutside', () => {
|
||||||
it('should trigger the callback when clicking outside the specified refs', () => {
|
it('should trigger the callback when clicking outside the specified refs', () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
|
||||||
renderHook(
|
renderHook(
|
||||||
() =>
|
() =>
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [containerRef],
|
||||||
callback,
|
callback,
|
||||||
listenerId,
|
listenerId,
|
||||||
@ -46,7 +46,7 @@ describe('useListenClickOutsideV2', () => {
|
|||||||
|
|
||||||
renderHook(
|
renderHook(
|
||||||
() =>
|
() =>
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [nullRef],
|
refs: [nullRef],
|
||||||
callback,
|
callback,
|
||||||
mode: ClickOutsideMode.comparePixels,
|
mode: ClickOutsideMode.comparePixels,
|
||||||
@ -68,7 +68,7 @@ describe('useListenClickOutsideV2', () => {
|
|||||||
|
|
||||||
renderHook(
|
renderHook(
|
||||||
() =>
|
() =>
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [containerRef],
|
||||||
callback,
|
callback,
|
||||||
listenerId,
|
listenerId,
|
||||||
@ -91,7 +91,7 @@ describe('useListenClickOutsideV2', () => {
|
|||||||
|
|
||||||
renderHook(
|
renderHook(
|
||||||
() =>
|
() =>
|
||||||
useListenClickOutsideV2({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [containerRef],
|
||||||
callback,
|
callback,
|
||||||
mode: ClickOutsideMode.comparePixels,
|
mode: ClickOutsideMode.comparePixels,
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import { fireEvent, renderHook } from '@testing-library/react';
|
|
||||||
import React from 'react';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ClickOutsideMode,
|
|
||||||
useListenClickOutside,
|
|
||||||
} from '../useListenClickOutside';
|
|
||||||
|
|
||||||
const containerRef = React.createRef<HTMLDivElement>();
|
|
||||||
const nullRef = React.createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<div ref={containerRef}>{children}</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useListenClickOutside', () => {
|
|
||||||
it('should trigger the callback when clicking outside the specified refs', () => {
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
renderHook(
|
|
||||||
() => useListenClickOutside({ refs: [containerRef], callback }),
|
|
||||||
{ wrapper: Wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.mouseDown(document);
|
|
||||||
fireEvent.click(document);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call the callback when clicking inside the specified refs using pixel comparison', () => {
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
renderHook(
|
|
||||||
() =>
|
|
||||||
useListenClickOutside({
|
|
||||||
refs: [containerRef, nullRef],
|
|
||||||
callback,
|
|
||||||
mode: ClickOutsideMode.comparePixels,
|
|
||||||
}),
|
|
||||||
{ wrapper: Wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
if (isDefined(containerRef.current)) {
|
|
||||||
fireEvent.mouseDown(containerRef.current);
|
|
||||||
fireEvent.click(containerRef.current);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the callback when clicking outside the specified refs using pixel comparison', () => {
|
|
||||||
const callback = jest.fn();
|
|
||||||
|
|
||||||
renderHook(() =>
|
|
||||||
useListenClickOutside({
|
|
||||||
refs: [containerRef, nullRef],
|
|
||||||
callback,
|
|
||||||
mode: ClickOutsideMode.comparePixels,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
// Simulate a click outside the specified refs
|
|
||||||
fireEvent.mouseDown(document.body);
|
|
||||||
fireEvent.click(document.body);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -2,10 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
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 { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback';
|
||||||
import { toSpliced } from '~/utils/array/toSpliced';
|
import { toSpliced } from '~/utils/array/toSpliced';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -17,35 +14,6 @@ export const useClickOutsideListener = (componentId: string) => {
|
|||||||
getClickOutsideListenerMouseDownHappenedState,
|
getClickOutsideListenerMouseDownHappenedState,
|
||||||
} = useClickOustideListenerStates(componentId);
|
} = useClickOustideListenerStates(componentId);
|
||||||
|
|
||||||
const useListenClickOutside = <T extends Element>({
|
|
||||||
callback,
|
|
||||||
refs,
|
|
||||||
enabled,
|
|
||||||
mode,
|
|
||||||
}: Omit<ClickOutsideListenerProps<T>, 'listenerId'>) => {
|
|
||||||
return useListenClickOutsideV2({
|
|
||||||
listenerId: componentId,
|
|
||||||
refs,
|
|
||||||
callback: useRecoilCallback(
|
|
||||||
({ snapshot }) =>
|
|
||||||
(event) => {
|
|
||||||
callback(event);
|
|
||||||
|
|
||||||
const additionalCallbacks = snapshot
|
|
||||||
.getLoadable(getClickOutsideListenerCallbacksState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
additionalCallbacks.forEach((additionalCallback) => {
|
|
||||||
additionalCallback.callbackFunction(event);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[callback],
|
|
||||||
),
|
|
||||||
enabled,
|
|
||||||
mode,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleClickOutsideListener = useRecoilCallback(
|
const toggleClickOutsideListener = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(activated: boolean) => {
|
(activated: boolean) => {
|
||||||
@ -152,7 +120,6 @@ export const useClickOutsideListener = (componentId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useListenClickOutside,
|
|
||||||
toggleClickOutsideListener,
|
toggleClickOutsideListener,
|
||||||
useRegisterClickOutsideListenerCallback,
|
useRegisterClickOutsideListenerCallback,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 {
|
export enum ClickOutsideMode {
|
||||||
comparePixels = 'comparePixels',
|
comparePixels = 'comparePixels',
|
||||||
compareHTMLRef = 'compareHTMLRef',
|
compareHTMLRef = 'compareHTMLRef',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useListenClickOutside = <T extends Element>({
|
export type ClickOutsideListenerProps<T extends Element> = {
|
||||||
refs,
|
|
||||||
callback,
|
|
||||||
mode = ClickOutsideMode.compareHTMLRef,
|
|
||||||
enabled = true,
|
|
||||||
}: {
|
|
||||||
refs: Array<React.RefObject<T>>;
|
refs: Array<React.RefObject<T>>;
|
||||||
|
excludeClassNames?: string[];
|
||||||
callback: (event: MouseEvent | TouchEvent) => void;
|
callback: (event: MouseEvent | TouchEvent) => void;
|
||||||
mode?: ClickOutsideMode;
|
mode?: ClickOutsideMode;
|
||||||
|
listenerId: string;
|
||||||
|
hotkeyScope?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}) => {
|
};
|
||||||
const [isMouseDownInside, setIsMouseDownInside] = useState(false);
|
|
||||||
|
export const useListenClickOutside = <T extends Element>({
|
||||||
|
refs,
|
||||||
|
excludeClassNames,
|
||||||
|
callback,
|
||||||
|
mode = ClickOutsideMode.compareHTMLRef,
|
||||||
|
listenerId,
|
||||||
|
hotkeyScope,
|
||||||
|
enabled = true,
|
||||||
|
}: ClickOutsideListenerProps<T>) => {
|
||||||
|
const {
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
getClickOutsideListenerIsActivatedState,
|
||||||
|
getClickOutsideListenerMouseDownHappenedState,
|
||||||
|
} = useClickOustideListenerStates(listenerId);
|
||||||
|
|
||||||
|
const handleMouseDown = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const clickOutsideListenerIsActivated = snapshot
|
||||||
|
.getLoadable(getClickOutsideListenerIsActivatedState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
set(getClickOutsideListenerMouseDownHappenedState, true);
|
||||||
|
|
||||||
|
const currentHotkeyScopes = snapshot
|
||||||
|
.getLoadable(internalHotkeysEnabledScopesState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const isListeningBasedOnHotkeyScope = hotkeyScope
|
||||||
|
? currentHotkeyScopes.includes(hotkeyScope)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const isListening =
|
||||||
|
clickOutsideListenerIsActivated &&
|
||||||
|
enabled &&
|
||||||
|
isListeningBasedOnHotkeyScope;
|
||||||
|
|
||||||
|
if (!isListening) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => ref.current?.contains(event.target as Node));
|
||||||
|
|
||||||
|
set(
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
clickedOnAtLeastOneRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.comparePixels) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y, width, height } =
|
||||||
|
ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const clientX =
|
||||||
|
'clientX' in event
|
||||||
|
? event.clientX
|
||||||
|
: event.changedTouches[0].clientX;
|
||||||
|
const clientY =
|
||||||
|
'clientY' in event
|
||||||
|
? event.clientY
|
||||||
|
: event.changedTouches[0].clientY;
|
||||||
|
|
||||||
|
if (
|
||||||
|
clientX < x ||
|
||||||
|
clientX > x + width ||
|
||||||
|
clientY < y ||
|
||||||
|
clientY > y + height
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
set(
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
clickedOnAtLeastOneRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getClickOutsideListenerIsActivatedState,
|
||||||
|
getClickOutsideListenerMouseDownHappenedState,
|
||||||
|
hotkeyScope,
|
||||||
|
enabled,
|
||||||
|
mode,
|
||||||
|
refs,
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickOutside = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const clickOutsideListenerIsActivated = snapshot
|
||||||
|
.getLoadable(getClickOutsideListenerIsActivatedState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const currentHotkeyScopes = snapshot
|
||||||
|
.getLoadable(internalHotkeysEnabledScopesState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const isListeningBasedOnHotkeyScope = hotkeyScope
|
||||||
|
? currentHotkeyScopes.includes(hotkeyScope)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const isListening =
|
||||||
|
clickOutsideListenerIsActivated &&
|
||||||
|
enabled &&
|
||||||
|
isListeningBasedOnHotkeyScope;
|
||||||
|
|
||||||
|
const isMouseDownInside = snapshot
|
||||||
|
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const hasMouseDownHappened = snapshot
|
||||||
|
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||||
|
const clickedElement = event.target as HTMLElement;
|
||||||
|
let isClickedOnExcluded = false;
|
||||||
|
let currentElement: HTMLElement | null = clickedElement;
|
||||||
|
|
||||||
|
while (currentElement) {
|
||||||
|
const currentClassList = currentElement.classList;
|
||||||
|
|
||||||
|
isClickedOnExcluded =
|
||||||
|
excludeClassNames?.some((className) =>
|
||||||
|
currentClassList.contains(className),
|
||||||
|
) ?? false;
|
||||||
|
|
||||||
|
if (isClickedOnExcluded) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => ref.current?.contains(event.target as Node));
|
||||||
|
|
||||||
|
if (
|
||||||
|
isListening &&
|
||||||
|
hasMouseDownHappened &&
|
||||||
|
!clickedOnAtLeastOneRef &&
|
||||||
|
!isMouseDownInside &&
|
||||||
|
!isClickedOnExcluded
|
||||||
|
) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === ClickOutsideMode.comparePixels) {
|
||||||
|
const clickedOnAtLeastOneRef = refs
|
||||||
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y, width, height } =
|
||||||
|
ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const clientX =
|
||||||
|
'clientX' in event
|
||||||
|
? event.clientX
|
||||||
|
: event.changedTouches[0].clientX;
|
||||||
|
const clientY =
|
||||||
|
'clientY' in event
|
||||||
|
? event.clientY
|
||||||
|
: event.changedTouches[0].clientY;
|
||||||
|
|
||||||
|
if (
|
||||||
|
clientX < x ||
|
||||||
|
clientX > x + width ||
|
||||||
|
clientY < y ||
|
||||||
|
clientY > y + height
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!clickedOnAtLeastOneRef &&
|
||||||
|
!isMouseDownInside &&
|
||||||
|
isListening &&
|
||||||
|
hasMouseDownHappened
|
||||||
|
) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getClickOutsideListenerIsActivatedState,
|
||||||
|
hotkeyScope,
|
||||||
|
enabled,
|
||||||
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
|
getClickOutsideListenerMouseDownHappenedState,
|
||||||
|
mode,
|
||||||
|
refs,
|
||||||
|
excludeClassNames,
|
||||||
|
callback,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseDown = (event: MouseEvent | TouchEvent) => {
|
document.addEventListener('mousedown', handleMouseDown, {
|
||||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
capture: true,
|
||||||
const clickedOnAtLeastOneRef = refs
|
});
|
||||||
.filter((ref) => !!ref.current)
|
document.addEventListener('click', handleClickOutside, { capture: true });
|
||||||
.some((ref) => ref.current?.contains(event.target as Node));
|
document.addEventListener('touchstart', handleMouseDown, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
document.addEventListener('touchend', handleClickOutside, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
setIsMouseDownInside(clickedOnAtLeastOneRef);
|
return () => {
|
||||||
}
|
document.removeEventListener('mousedown', handleMouseDown, {
|
||||||
|
capture: true,
|
||||||
if (mode === ClickOutsideMode.comparePixels) {
|
});
|
||||||
const clickedOnAtLeastOneRef = refs
|
document.removeEventListener('click', handleClickOutside, {
|
||||||
.filter((ref) => !!ref.current)
|
capture: true,
|
||||||
.some((ref) => {
|
});
|
||||||
if (!ref.current) {
|
document.removeEventListener('touchstart', handleMouseDown, {
|
||||||
return false;
|
capture: true,
|
||||||
}
|
});
|
||||||
|
document.removeEventListener('touchend', handleClickOutside, {
|
||||||
const { x, y, width, height } = ref.current.getBoundingClientRect();
|
capture: true,
|
||||||
|
});
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}, [refs, callback, mode, handleClickOutside, handleMouseDown]);
|
||||||
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]);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,266 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useRecoilCallback } from 'recoil';
|
|
||||||
|
|
||||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
|
||||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
|
||||||
|
|
||||||
export enum ClickOutsideMode {
|
|
||||||
comparePixels = 'comparePixels',
|
|
||||||
compareHTMLRef = 'compareHTMLRef',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClickOutsideListenerProps<T extends Element> = {
|
|
||||||
refs: Array<React.RefObject<T>>;
|
|
||||||
excludeClassNames?: string[];
|
|
||||||
callback: (event: MouseEvent | TouchEvent) => void;
|
|
||||||
mode?: ClickOutsideMode;
|
|
||||||
listenerId: string;
|
|
||||||
hotkeyScope?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useListenClickOutsideV2 = <T extends Element>({
|
|
||||||
refs,
|
|
||||||
excludeClassNames,
|
|
||||||
callback,
|
|
||||||
mode = ClickOutsideMode.compareHTMLRef,
|
|
||||||
listenerId,
|
|
||||||
hotkeyScope,
|
|
||||||
enabled = true,
|
|
||||||
}: ClickOutsideListenerProps<T>) => {
|
|
||||||
const {
|
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
|
||||||
getClickOutsideListenerIsActivatedState,
|
|
||||||
getClickOutsideListenerMouseDownHappenedState,
|
|
||||||
} = useClickOustideListenerStates(listenerId);
|
|
||||||
|
|
||||||
const handleMouseDown = useRecoilCallback(
|
|
||||||
({ snapshot, set }) =>
|
|
||||||
(event: MouseEvent | TouchEvent) => {
|
|
||||||
const clickOutsideListenerIsActivated = snapshot
|
|
||||||
.getLoadable(getClickOutsideListenerIsActivatedState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
set(getClickOutsideListenerMouseDownHappenedState, true);
|
|
||||||
|
|
||||||
const currentHotkeyScopes = snapshot
|
|
||||||
.getLoadable(internalHotkeysEnabledScopesState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const isListeningBasedOnHotkeyScope = hotkeyScope
|
|
||||||
? currentHotkeyScopes.includes(hotkeyScope)
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const isListening =
|
|
||||||
clickOutsideListenerIsActivated &&
|
|
||||||
enabled &&
|
|
||||||
isListeningBasedOnHotkeyScope;
|
|
||||||
|
|
||||||
if (!isListening) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
|
||||||
const clickedOnAtLeastOneRef = refs
|
|
||||||
.filter((ref) => !!ref.current)
|
|
||||||
.some((ref) => ref.current?.contains(event.target as Node));
|
|
||||||
|
|
||||||
set(
|
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
|
||||||
clickedOnAtLeastOneRef,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === ClickOutsideMode.comparePixels) {
|
|
||||||
const clickedOnAtLeastOneRef = refs
|
|
||||||
.filter((ref) => !!ref.current)
|
|
||||||
.some((ref) => {
|
|
||||||
if (!ref.current) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y, width, height } =
|
|
||||||
ref.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
const clientX =
|
|
||||||
'clientX' in event
|
|
||||||
? event.clientX
|
|
||||||
: event.changedTouches[0].clientX;
|
|
||||||
const clientY =
|
|
||||||
'clientY' in event
|
|
||||||
? event.clientY
|
|
||||||
: event.changedTouches[0].clientY;
|
|
||||||
|
|
||||||
if (
|
|
||||||
clientX < x ||
|
|
||||||
clientX > x + width ||
|
|
||||||
clientY < y ||
|
|
||||||
clientY > y + height
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
set(
|
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
|
||||||
clickedOnAtLeastOneRef,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
getClickOutsideListenerIsActivatedState,
|
|
||||||
getClickOutsideListenerMouseDownHappenedState,
|
|
||||||
hotkeyScope,
|
|
||||||
enabled,
|
|
||||||
mode,
|
|
||||||
refs,
|
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickOutside = useRecoilCallback(
|
|
||||||
({ snapshot }) =>
|
|
||||||
(event: MouseEvent | TouchEvent) => {
|
|
||||||
const clickOutsideListenerIsActivated = snapshot
|
|
||||||
.getLoadable(getClickOutsideListenerIsActivatedState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const currentHotkeyScopes = snapshot
|
|
||||||
.getLoadable(internalHotkeysEnabledScopesState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const isListeningBasedOnHotkeyScope = hotkeyScope
|
|
||||||
? currentHotkeyScopes.includes(hotkeyScope)
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const isListening =
|
|
||||||
clickOutsideListenerIsActivated &&
|
|
||||||
enabled &&
|
|
||||||
isListeningBasedOnHotkeyScope;
|
|
||||||
|
|
||||||
const isMouseDownInside = snapshot
|
|
||||||
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const hasMouseDownHappened = snapshot
|
|
||||||
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
|
||||||
const clickedElement = event.target as HTMLElement;
|
|
||||||
let isClickedOnExcluded = false;
|
|
||||||
let currentElement: HTMLElement | null = clickedElement;
|
|
||||||
|
|
||||||
while (currentElement) {
|
|
||||||
const currentClassList = currentElement.classList;
|
|
||||||
|
|
||||||
isClickedOnExcluded =
|
|
||||||
excludeClassNames?.some((className) =>
|
|
||||||
currentClassList.contains(className),
|
|
||||||
) ?? false;
|
|
||||||
|
|
||||||
if (isClickedOnExcluded) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentElement = currentElement.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickedOnAtLeastOneRef = refs
|
|
||||||
.filter((ref) => !!ref.current)
|
|
||||||
.some((ref) => ref.current?.contains(event.target as Node));
|
|
||||||
|
|
||||||
if (
|
|
||||||
isListening &&
|
|
||||||
hasMouseDownHappened &&
|
|
||||||
!clickedOnAtLeastOneRef &&
|
|
||||||
!isMouseDownInside &&
|
|
||||||
!isClickedOnExcluded
|
|
||||||
) {
|
|
||||||
callback(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === ClickOutsideMode.comparePixels) {
|
|
||||||
const clickedOnAtLeastOneRef = refs
|
|
||||||
.filter((ref) => !!ref.current)
|
|
||||||
.some((ref) => {
|
|
||||||
if (!ref.current) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y, width, height } =
|
|
||||||
ref.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
const clientX =
|
|
||||||
'clientX' in event
|
|
||||||
? event.clientX
|
|
||||||
: event.changedTouches[0].clientX;
|
|
||||||
const clientY =
|
|
||||||
'clientY' in event
|
|
||||||
? event.clientY
|
|
||||||
: event.changedTouches[0].clientY;
|
|
||||||
|
|
||||||
if (
|
|
||||||
clientX < x ||
|
|
||||||
clientX > x + width ||
|
|
||||||
clientY < y ||
|
|
||||||
clientY > y + height
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!clickedOnAtLeastOneRef &&
|
|
||||||
!isMouseDownInside &&
|
|
||||||
isListening &&
|
|
||||||
hasMouseDownHappened
|
|
||||||
) {
|
|
||||||
callback(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
getClickOutsideListenerIsActivatedState,
|
|
||||||
hotkeyScope,
|
|
||||||
enabled,
|
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
|
||||||
getClickOutsideListenerMouseDownHappenedState,
|
|
||||||
mode,
|
|
||||||
refs,
|
|
||||||
excludeClassNames,
|
|
||||||
callback,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('mousedown', handleMouseDown, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
document.addEventListener('click', handleClickOutside, { capture: true });
|
|
||||||
document.addEventListener('touchstart', handleMouseDown, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
document.addEventListener('touchend', handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleMouseDown, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
document.removeEventListener('click', handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
document.removeEventListener('touchstart', handleMouseDown, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
document.removeEventListener('touchend', handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [refs, callback, mode, handleClickOutside, handleMouseDown]);
|
|
||||||
};
|
|
||||||
@ -3,7 +3,7 @@ import {
|
|||||||
OnDragEndResponder,
|
OnDragEndResponder,
|
||||||
ResponderProvided,
|
ResponderProvided,
|
||||||
} from '@hello-pangea/dnd';
|
} from '@hello-pangea/dnd';
|
||||||
import { useRef, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
AppTooltip,
|
AppTooltip,
|
||||||
@ -20,7 +20,6 @@ import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableIt
|
|||||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
||||||
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
|
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -80,15 +79,6 @@ export const ViewFieldsVisibilityDropdownSection = ({
|
|||||||
return iconButtons.length ? iconButtons : undefined;
|
return iconButtons.length ? iconButtons : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useListenClickOutside({
|
|
||||||
refs: [ref],
|
|
||||||
callback: () => {
|
|
||||||
setOpenToolTipIndex(undefined);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { nonDraggableItems = [], draggableItems = [] } = isDraggable
|
const { nonDraggableItems = [], draggableItems = [] } = isDraggable
|
||||||
? groupArrayItemsBy(fields, ({ isLabelIdentifier }) =>
|
? groupArrayItemsBy(fields, ({ isLabelIdentifier }) =>
|
||||||
isLabelIdentifier ? 'nonDraggableItems' : 'draggableItems',
|
isLabelIdentifier ? 'nonDraggableItems' : 'draggableItems',
|
||||||
@ -96,7 +86,7 @@ export const ViewFieldsVisibilityDropdownSection = ({
|
|||||||
: { nonDraggableItems: fields, draggableItems: [] };
|
: { nonDraggableItems: fields, draggableItems: [] };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<>
|
||||||
{showSubheader && (
|
{showSubheader && (
|
||||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||||
)}
|
)}
|
||||||
@ -158,6 +148,6 @@ export const ViewFieldsVisibilityDropdownSection = ({
|
|||||||
/>,
|
/>,
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,3 +5,4 @@ export * from './decorators/ComponentWithRouterDecorator';
|
|||||||
export * from './decorators/RouterDecorator';
|
export * from './decorators/RouterDecorator';
|
||||||
export * from './mocks/avatarUrlMock';
|
export * from './mocks/avatarUrlMock';
|
||||||
export * from './types/CatalogStory';
|
export * from './types/CatalogStory';
|
||||||
|
export * from './utils/getCanvasElementForDropdownTesting';
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user