Fixed many dropdown bugs (#8256)

Many dropdown bugs have been fixed, more refactoring is needed.

Dropdown fixed : 
- Filter select
- Sort select
- Visible field select
- Hidden field select
- Multi item picker (phones, links, emails, etc.)
- Phone country select
This commit is contained in:
Lucas Bordeau
2024-11-01 09:23:01 +01:00
committed by GitHub
parent a287edd91b
commit c93d2bcd5e
15 changed files with 276 additions and 182 deletions

View File

@@ -5,15 +5,10 @@ import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
const StyledContainer = styled.div`
position: relative;
`;
type MultipleFiltersDropdownContentProps = { type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string; filterDropdownId?: string;
}; };
@@ -46,7 +41,7 @@ export const MultipleFiltersDropdownContent = ({
const shoudShowFilterInput = objectFilterDropdownFilterIsSelected; const shoudShowFilterInput = objectFilterDropdownFilterIsSelected;
return ( return (
<StyledContainer> <>
{shoudShowFilterInput ? ( {shoudShowFilterInput ? (
<ObjectFilterOperandSelectAndInput <ObjectFilterOperandSelectAndInput
filterDropdownId={filterDropdownId} filterDropdownId={filterDropdownId}
@@ -61,6 +56,6 @@ export const MultipleFiltersDropdownContent = ({
filterDefinitionUsedInDropdown?.type filterDefinitionUsedInDropdown?.type
} }
/> />
</StyledContainer> </>
); );
}; };

View File

@@ -28,8 +28,13 @@ export const ObjectFilterDropdownFilterInput = ({
const { const {
filterDefinitionUsedInDropdownState, filterDefinitionUsedInDropdownState,
selectedOperandInDropdownState, selectedOperandInDropdownState,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown({ filterDropdownId }); } = useFilterDropdown({ filterDropdownId });
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const filterDefinitionUsedInDropdown = useRecoilValue( const filterDefinitionUsedInDropdown = useRecoilValue(
filterDefinitionUsedInDropdownState, filterDefinitionUsedInDropdownState,
); );
@@ -53,7 +58,9 @@ export const ObjectFilterDropdownFilterInput = ({
ViewFilterOperand.IsRelative, ViewFilterOperand.IsRelative,
].includes(selectedOperandInDropdown); ].includes(selectedOperandInDropdown);
if (!isDefined(filterDefinitionUsedInDropdown)) { const shouldHide = isObjectFilterDropdownOperandSelectUnfolded;
if (shouldHide || !isDefined(filterDefinitionUsedInDropdown)) {
return null; return null;
} }

View File

@@ -8,9 +8,7 @@ const StyledOperandSelectContainer = styled.div`
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light}; box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md}; border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
width: 100%; width: 100%;
z-index: 1000; z-index: 1000;
`; `;

View File

@@ -16,6 +16,7 @@ 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';
@@ -35,7 +36,7 @@ export const StyledInput = styled.input`
margin: 0; margin: 0;
outline: none; outline: none;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
height: 19px; min-height: 19px;
font-family: inherit; font-family: inherit;
font-size: ${({ theme }) => theme.font.size.sm}; font-size: ${({ theme }) => theme.font.size.sm};
@@ -160,6 +161,7 @@ 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}
@@ -197,6 +199,7 @@ export const ObjectFilterDropdownFilterSelect = ({
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</SelectableList> </SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />} {shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
</ScrollWrapper>
</> </>
); );
}; };

View File

@@ -10,17 +10,28 @@ export const ObjectFilterDropdownOperandButton = () => {
const { const {
selectedOperandInDropdownState, selectedOperandInDropdownState,
setIsObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown(); } = useFilterDropdown();
const selectedOperandInDropdown = useRecoilValue( const selectedOperandInDropdown = useRecoilValue(
selectedOperandInDropdownState, selectedOperandInDropdownState,
); );
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const handleButtonClick = () => {
setIsObjectFilterDropdownOperandSelectUnfolded(
!isObjectFilterDropdownOperandSelectUnfolded,
);
};
return ( return (
<DropdownMenuHeader <DropdownMenuHeader
key={'selected-filter-operand'} key={'selected-filter-operand'}
EndIcon={IconChevronDown} EndIcon={IconChevronDown}
onClick={() => setIsObjectFilterDropdownOperandSelectUnfolded(true)} onClick={handleButtonClick}
> >
{getOperandLabel(selectedOperandInDropdown)} {getOperandLabel(selectedOperandInDropdown)}
</DropdownMenuHeader> </DropdownMenuHeader>

View File

@@ -16,6 +16,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelect } from '@/ui/navigation/menu-item/components/MenuItemMultiSelect'; import { MenuItemMultiSelect } from '@/ui/navigation/menu-item/components/MenuItemMultiSelect';
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 { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const EMPTY_FILTER_VALUE = ''; export const EMPTY_FILTER_VALUE = '';
@@ -162,6 +163,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
} }
}} }}
> >
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => ( {optionsInDropdown?.map((option) => (
<MenuItemMultiSelect <MenuItemMultiSelect
@@ -178,6 +180,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
))} ))}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />} {showNoResult && <MenuItem text="No result" />}
</ScrollWrapper>
</SelectableList> </SelectableList>
); );
}; };

View File

@@ -15,6 +15,7 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/Styl
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
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 { useContext } from 'react'; import { useContext } from 'react';
import { SORT_DIRECTIONS } from '../types/SortDirection'; import { SORT_DIRECTIONS } from '../types/SortDirection';
@@ -42,17 +43,13 @@ export const StyledInput = styled.input`
} }
`; `;
const StyledContainer = styled.div`
position: relative;
`;
const StyledSelectedSortDirectionContainer = styled.div` const StyledSelectedSortDirectionContainer = styled.div`
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light}; box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md}; border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute; position: absolute;
top: 10px; top: 32px;
width: 100%; width: 100%;
z-index: 1000; z-index: 1000;
`; `;
@@ -166,10 +163,11 @@ export const ObjectSortDropdownButton = ({
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</StyledSelectedSortDirectionContainer> </StyledSelectedSortDirectionContainer>
)} )}
<StyledContainer>
<DropdownMenuHeader <DropdownMenuHeader
EndIcon={IconChevronDown} EndIcon={IconChevronDown}
onClick={() => setIsSortDirectionMenuUnfolded(true)} onClick={() =>
setIsSortDirectionMenuUnfolded(!isSortDirectionMenuUnfolded)
}
> >
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader> </DropdownMenuHeader>
@@ -181,6 +179,7 @@ export const ObjectSortDropdownButton = ({
setObjectSortDropdownSearchInput(event.target.value) setObjectSortDropdownSearchInput(event.target.value)
} }
/> />
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{visibleColumnsSortDefinitions.map( {visibleColumnsSortDefinitions.map(
(visibleSortDefinition, index) => ( (visibleSortDefinition, index) => (
@@ -214,7 +213,7 @@ export const ObjectSortDropdownButton = ({
), ),
)} )}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</StyledContainer> </ScrollWrapper>
</> </>
} }
onClose={handleDropdownButtonClose} onClose={handleDropdownButtonClose}

View File

@@ -21,7 +21,6 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
const StyledDropdownMenu = styled(DropdownMenu)` const StyledDropdownMenu = styled(DropdownMenu)`
left: -1px; left: -1px;
position: absolute;
top: -1px; top: -1px;
`; `;
@@ -46,6 +45,7 @@ type MultiItemFieldInputProps<T> = {
}; };
// 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, ...
// This should be refactored with a hook instead that exposes those events in a context around this component and its children.
export const MultiItemFieldInput = <T,>({ export const MultiItemFieldInput = <T,>({
items, items,
onPersist, onPersist,

View File

@@ -1,14 +1,11 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import styled from '@emotion/styled'; import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { import {
IconBookmark, IconBookmark,
IconBookmarkPlus, IconBookmarkPlus,
IconComponent,
IconDotsVertical,
IconPencil, IconPencil,
IconTrash, IconTrash,
} from 'twenty-ui'; } from 'twenty-ui';
@@ -24,12 +21,6 @@ type MultiItemFieldMenuItemProps<T> = {
hasPrimaryButton?: boolean; hasPrimaryButton?: boolean;
}; };
const StyledIconBookmark = styled(IconBookmark)`
color: ${({ theme }) => theme.font.color.light};
height: ${({ theme }) => theme.icon.size.sm}px;
width: ${({ theme }) => theme.icon.size.sm}px;
`;
export const MultiItemFieldMenuItem = <T,>({ export const MultiItemFieldMenuItem = <T,>({
dropdownId, dropdownId,
isPrimary, isPrimary,
@@ -47,46 +38,42 @@ export const MultiItemFieldMenuItem = <T,>({
const handleMouseLeave = () => setIsHovered(false); const handleMouseLeave = () => setIsHovered(false);
const handleDeleteClick = () => { const handleDeleteClick = () => {
closeDropdown();
setIsHovered(false); setIsHovered(false);
onDelete?.(); onDelete?.();
}; };
useEffect(() => { const handleSetAsPrimaryClick = () => {
if (isDropdownOpen) { closeDropdown();
return () => closeDropdown(); onSetAsPrimary?.();
} };
}, [closeDropdown, isDropdownOpen]);
const handleEditClick = () => {
closeDropdown();
onEdit?.();
};
return ( return (
<MenuItem <MenuItemWithOptionDropdown
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
text={<DisplayComponent value={value} />} text={<DisplayComponent value={value} />}
isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen} isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
iconButtons={[ RightIcon={isHovered ? null : IconBookmark}
{
Wrapper: isHovered
? ({ iconButton }) => (
<Dropdown
dropdownId={dropdownId} dropdownId={dropdownId}
dropdownHotkeyScope={{ scope: dropdownId }} dropdownContent={
dropdownPlacement="right-start"
dropdownStrategy="fixed"
disableBlur
clickableComponent={iconButton}
dropdownComponents={
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{hasPrimaryButton && !isPrimary && ( {hasPrimaryButton && !isPrimary && (
<MenuItem <MenuItem
LeftIcon={IconBookmarkPlus} LeftIcon={IconBookmarkPlus}
text="Set as Primary" text="Set as Primary"
onClick={onSetAsPrimary} onClick={handleSetAsPrimaryClick}
/> />
)} )}
<MenuItem <MenuItem
LeftIcon={IconPencil} LeftIcon={IconPencil}
text="Edit" text="Edit"
onClick={onEdit} onClick={handleEditClick}
/> />
<MenuItem <MenuItem
accent="danger" accent="danger"
@@ -97,16 +84,5 @@ export const MultiItemFieldMenuItem = <T,>({
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
} }
/> />
)
: undefined,
Icon:
isPrimary && !isHovered
? (StyledIconBookmark as IconComponent)
: IconDotsVertical,
accent: 'tertiary',
onClick: isHovered ? () => {} : undefined,
},
]}
/>
); );
}; };

View File

@@ -40,6 +40,7 @@ import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemN
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle'; import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
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 { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection'; import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
@@ -259,6 +260,7 @@ export const RecordIndexOptionsDropdownContent = ({
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}> <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Fields Fields
</DropdownMenuHeader> </DropdownMenuHeader>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ViewFieldsVisibilityDropdownSection <ViewFieldsVisibilityDropdownSection
title="Visible" title="Visible"
fields={visibleRecordFields} fields={visibleRecordFields}
@@ -268,6 +270,7 @@ export const RecordIndexOptionsDropdownContent = ({
showSubheader={false} showSubheader={false}
showDragGrip={true} showDragGrip={true}
/> />
</ScrollWrapper>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItemNavigate <MenuItemNavigate
@@ -317,7 +320,7 @@ export const RecordIndexOptionsDropdownContent = ({
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}
@@ -326,7 +329,7 @@ export const RecordIndexOptionsDropdownContent = ({
showSubheader={false} showSubheader={false}
showDragGrip={false} showDragGrip={false}
/> />
</> </ScrollWrapper>
)} )}
<DropdownMenuSeparator /> <DropdownMenuSeparator />

View File

@@ -74,8 +74,8 @@ export const PhoneCountryPickerDropdownButton = ({
); );
const handleChange = (countryCode: string) => { const handleChange = (countryCode: string) => {
onChange(countryCode);
closeDropdown(); closeDropdown();
onChange(countryCode);
}; };
const countries = useCountries(); const countries = useCountries();
@@ -89,7 +89,6 @@ export const PhoneCountryPickerDropdownButton = ({
return ( return (
<Dropdown <Dropdown
dropdownMenuWidth={'100%'}
dropdownId="country-picker-dropdown-id" dropdownId="country-picker-dropdown-id"
dropdownHotkeyScope={{ scope: CountryPickerHotkeyScope.CountryPicker }} dropdownHotkeyScope={{ scope: CountryPickerHotkeyScope.CountryPicker }}
clickableComponent={ clickableComponent={

View File

@@ -7,7 +7,7 @@ import {
size, size,
useFloating, useFloating,
} from '@floating-ui/react'; } from '@floating-ui/react';
import { MouseEvent, useEffect, useRef } from 'react'; import { MouseEvent, ReactNode, useRef } from 'react';
import { Keys } from 'react-hotkeys-hook'; import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
@@ -27,8 +27,8 @@ import { DropdownOnToggleEffect } from './DropdownOnToggleEffect';
type DropdownProps = { type DropdownProps = {
className?: string; className?: string;
clickableComponent?: JSX.Element | JSX.Element[]; clickableComponent?: ReactNode;
dropdownComponents: JSX.Element | JSX.Element[]; dropdownComponents: ReactNode;
hotkey?: { hotkey?: {
key: Keys; key: Keys;
scope: string; scope: string;
@@ -65,13 +65,8 @@ export const Dropdown = ({
}: DropdownProps) => { }: DropdownProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { const { isDropdownOpen, toggleDropdown, closeDropdown, dropdownWidth } =
isDropdownOpen, useDropdown(dropdownId);
toggleDropdown,
closeDropdown,
dropdownWidth,
setDropdownPlacement,
} = useDropdown(dropdownId);
const offsetMiddlewares = []; const offsetMiddlewares = [];
@@ -83,18 +78,21 @@ export const Dropdown = ({
offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y })); offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y }));
} }
const { refs, floatingStyles, placement } = useFloating({ const { refs, floatingStyles } = useFloating({
placement: dropdownPlacement, placement: dropdownPlacement,
middleware: [ middleware: [
flip(), flip(),
size({ size({
padding: 12 + 20, // 12px for padding bottom, 20px for dropdown bottom margin target padding: 32,
apply: ({ availableHeight, elements }) => { apply: ({ availableHeight, elements }) => {
elements.floating.style.maxHeight = elements.floating.style.maxHeight =
availableHeight >= elements.floating.scrollHeight availableHeight >= elements.floating.scrollHeight
? '' ? ''
: `${availableHeight}px`; : `${availableHeight}px`;
elements.floating.style.height = 'auto';
}, },
boundary: document.querySelector('#root') ?? undefined,
}), }),
...offsetMiddlewares, ...offsetMiddlewares,
], ],
@@ -102,10 +100,6 @@ export const Dropdown = ({
strategy: dropdownStrategy, strategy: dropdownStrategy,
}); });
useEffect(() => {
setDropdownPlacement(placement);
}, [placement, setDropdownPlacement]);
const handleHotkeyTriggered = () => { const handleHotkeyTriggered = () => {
toggleDropdown(); toggleDropdown();
}; };

View File

@@ -23,10 +23,10 @@ const StyledDropdownMenu = styled.div<{
display: flex; display: flex;
height: 100%;
flex-direction: column; flex-direction: column;
z-index: 30; z-index: 30;
overflow-y: auto;
overflow-x: hidden;
width: ${({ width = 160 }) => width: ${({ width = 160 }) =>
typeof width === 'number' ? `${width}px` : width}; typeof width === 'number' ? `${width}px` : width};
`; `;

View File

@@ -13,7 +13,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')}; max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')};
overflow-y: auto;
padding: var(--padding); padding: var(--padding);
@@ -34,6 +33,8 @@ const StyledDropdownMenuItemsInternalContainer = styled.div`
width: 100%; width: 100%;
`; `;
// TODO: refactor this, the dropdown should handle the max height behavior + scroll with the size middleware
// We should instead create a DropdownMenuItemsContainerScrollable or take for granted that it is the default behavior
export const DropdownMenuItemsContainer = ({ export const DropdownMenuItemsContainer = ({
children, children,
hasMaxHeight, hasMaxHeight,

View File

@@ -0,0 +1,105 @@
import { useTheme } from '@emotion/react';
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
import {
IconChevronRight,
IconComponent,
IconDotsVertical,
LightIconButton,
LightIconButtonProps,
} from 'twenty-ui';
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
Icon: IconComponent;
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
};
export type MenuItemWithOptionDropdownProps = {
accent?: MenuItemAccent;
className?: string;
dropdownContent: ReactNode;
dropdownId: string;
isIconDisplayedOnHoverOnly?: boolean;
isTooltipOpen?: boolean;
LeftIcon?: IconComponent | null;
RightIcon?: IconComponent | null;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
testId?: string;
text: ReactNode;
hasSubMenu?: boolean;
};
// TODO: refactor this
export const MenuItemWithOptionDropdown = ({
accent = 'default',
className,
isIconDisplayedOnHoverOnly = true,
dropdownContent,
dropdownId,
LeftIcon,
RightIcon,
onClick,
onMouseEnter,
onMouseLeave,
testId,
text,
hasSubMenu = false,
}: MenuItemWithOptionDropdownProps) => {
const theme = useTheme();
const handleMenuItemClick = (event: MouseEvent<HTMLDivElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledHoverableMenuItemBase
data-testid={testId ?? undefined}
onClick={handleMenuItemClick}
className={className}
accent={accent}
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
<Dropdown
clickableComponent={
<LightIconButton
Icon={RightIcon ?? IconDotsVertical}
size="small"
/>
}
dropdownComponents={dropdownContent}
dropdownId={dropdownId}
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
disableBlur
/>
</div>
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledHoverableMenuItemBase>
);
};