- Removed disableBlur property from dropdown because it is no longer needed since there's only one OverlayContainer component so there can be only one blur at a time. - Removed blur CSS properties from every component that used it because one standalone OverlayContainer is able to handle all cases if placed properly. - Also removed disableBackgroundBlur property from SingleRecordSelect - Removed FieldInputOverlay and FieldTextAreaOverlay components that were a first attempt to create something like an OverlayContainer - Used new unified OverlayContainer in RecordInlineCell and RecordTableCell - Fixed ScrollWrapper so that it works well both for dropdown with non overflowing content and dropdown with overflowing content. - Removed export default value on SearchVariablesDropdown as it is not used in this codebase - Refactored SearchVariablesDropdown function as component anti-pattern - Refactored SearchVariablesDropdownFieldItems UI problems with separator and missing ScrollWrapper behavior - Refactored SearchVariablesDropdownObjectItems with UI problems with separator and missing ScrollWrapper behavior - Fixed blur bug on Firefox due to wrong placement of the element that had the CSS property. Blur works on Firefox it it's on the container that has the highest level in the tree. - Fixed bug in ActivityTargetInlineCell by removing an unnecessary container component StyledSelectContainer - Unified problems of field height with a new common component FieldInputContainer, instead of putting width and height at the wrong abstraction level, width and height are a field's concern not a dropdown, overlay or low-level input concern. - Fixed block editor dropdown with new OverlayContainer - Aligning field dropdown with their anchor on inline and table cells, there are still many small pixel misalignments that give a low quality impression. - Fixed FormDateFieldInput that was missing OverlayContainer
230 lines
7.0 KiB
TypeScript
230 lines
7.0 KiB
TypeScript
import styled from '@emotion/styled';
|
|
import { useMemo, useState } from 'react';
|
|
import { useRecoilValue } from 'recoil';
|
|
import {
|
|
IconApps,
|
|
IconButton,
|
|
IconButtonVariant,
|
|
IconComponent,
|
|
LightIconButton,
|
|
useIcons,
|
|
} from 'twenty-ui';
|
|
|
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
|
import { arrayToChunks } from '~/utils/array/arrayToChunks';
|
|
|
|
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
|
|
|
|
export type IconPickerProps = {
|
|
disabled?: boolean;
|
|
dropdownId?: string;
|
|
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
|
|
selectedIconKey?: string;
|
|
onClickOutside?: () => void;
|
|
onClose?: () => void;
|
|
onOpen?: () => void;
|
|
variant?: IconButtonVariant;
|
|
className?: string;
|
|
};
|
|
|
|
const StyledMenuIconItemsContainer = styled.div`
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
gap: ${({ theme }) => theme.spacing(0.5)};
|
|
`;
|
|
|
|
const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
|
|
background: ${({ theme, isSelected }) =>
|
|
isSelected ? theme.background.transparent.medium : 'transparent'};
|
|
`;
|
|
|
|
const convertIconKeyToLabel = (iconKey: string) =>
|
|
iconKey.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim();
|
|
|
|
type IconPickerIconProps = {
|
|
iconKey: string;
|
|
onClick: () => void;
|
|
selectedIconKey?: string;
|
|
Icon: IconComponent;
|
|
};
|
|
|
|
const IconPickerIcon = ({
|
|
iconKey,
|
|
onClick,
|
|
selectedIconKey,
|
|
Icon,
|
|
}: IconPickerIconProps) => {
|
|
const { isSelectedItemIdSelector } = useSelectableList();
|
|
|
|
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(iconKey));
|
|
|
|
return (
|
|
<StyledLightIconButton
|
|
key={iconKey}
|
|
aria-label={convertIconKeyToLabel(iconKey)}
|
|
size="medium"
|
|
title={iconKey}
|
|
isSelected={iconKey === selectedIconKey || isSelectedItemId}
|
|
Icon={Icon}
|
|
onClick={onClick}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const IconPicker = ({
|
|
disabled,
|
|
dropdownId = 'icon-picker',
|
|
onChange,
|
|
selectedIconKey,
|
|
onClickOutside,
|
|
onClose,
|
|
onOpen,
|
|
variant = 'secondary',
|
|
className,
|
|
}: IconPickerProps) => {
|
|
const [searchString, setSearchString] = useState('');
|
|
const {
|
|
goBackToPreviousHotkeyScope,
|
|
setHotkeyScopeAndMemorizePreviousScope,
|
|
} = usePreviousHotkeyScope();
|
|
|
|
const { closeDropdown } = useDropdown(dropdownId);
|
|
|
|
const { getIcons, getIcon } = useIcons();
|
|
const icons = getIcons();
|
|
const matchingSearchIconKeys = useMemo(() => {
|
|
if (icons == null) return [];
|
|
const scoreIconMatch = (iconKey: string, searchString: string) => {
|
|
const iconLabel = convertIconKeyToLabel(iconKey)
|
|
.toLowerCase()
|
|
.replace('icon ', '')
|
|
.replace(/\s/g, '');
|
|
|
|
const searchLower = searchString
|
|
.toLowerCase()
|
|
.trimEnd()
|
|
.replace(/\s/g, '');
|
|
|
|
if (iconKey === searchString || iconLabel === searchString) return 100;
|
|
if (iconKey.startsWith(searchLower) || iconLabel.startsWith(searchLower))
|
|
return 75;
|
|
if (iconKey.includes(searchLower) || iconLabel.includes(searchLower))
|
|
return 50;
|
|
|
|
return 0;
|
|
};
|
|
const scoredIcons = Object.keys(icons).map((iconKey) => ({
|
|
iconKey,
|
|
score: scoreIconMatch(iconKey, searchString),
|
|
}));
|
|
|
|
const filteredAndSortedIconKeys = scoredIcons
|
|
.filter(({ score }) => score > 0)
|
|
.sort((a, b) => b.score - a.score)
|
|
.map(({ iconKey }) => iconKey);
|
|
|
|
const isSelectedIconMatchingFilter =
|
|
selectedIconKey && filteredAndSortedIconKeys.includes(selectedIconKey);
|
|
|
|
return isSelectedIconMatchingFilter
|
|
? [
|
|
selectedIconKey,
|
|
...filteredAndSortedIconKeys.filter(
|
|
(iconKey) => iconKey !== selectedIconKey,
|
|
),
|
|
].slice(0, 25)
|
|
: filteredAndSortedIconKeys.slice(0, 25);
|
|
}, [icons, searchString, selectedIconKey]);
|
|
|
|
const iconKeys2d = useMemo(
|
|
() => arrayToChunks(matchingSearchIconKeys.slice(), 5),
|
|
[matchingSearchIconKeys],
|
|
);
|
|
|
|
const icon = selectedIconKey ? getIcon(selectedIconKey) : IconApps;
|
|
|
|
return (
|
|
<div className={className}>
|
|
<Dropdown
|
|
dropdownId={dropdownId}
|
|
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
|
|
clickableComponent={
|
|
<IconButton
|
|
ariaLabel={`Click to select icon ${
|
|
selectedIconKey
|
|
? `(selected: ${selectedIconKey})`
|
|
: `(no icon selected)`
|
|
}`}
|
|
disabled={disabled}
|
|
Icon={icon}
|
|
variant={variant}
|
|
/>
|
|
}
|
|
dropdownMenuWidth={176}
|
|
dropdownComponents={
|
|
<SelectableList
|
|
selectableListId="icon-list"
|
|
selectableItemIdMatrix={iconKeys2d}
|
|
hotkeyScope={IconPickerHotkeyScope.IconPicker}
|
|
onEnter={(iconKey) => {
|
|
onChange({ iconKey, Icon: getIcon(iconKey) });
|
|
closeDropdown();
|
|
}}
|
|
>
|
|
<DropdownMenu width={176}>
|
|
<DropdownMenuSearchInput
|
|
placeholder="Search icon"
|
|
autoFocus
|
|
onChange={(event) => {
|
|
setSearchString(event.target.value);
|
|
}}
|
|
/>
|
|
<DropdownMenuSeparator />
|
|
<div
|
|
onMouseEnter={() => {
|
|
setHotkeyScopeAndMemorizePreviousScope(
|
|
IconPickerHotkeyScope.IconPicker,
|
|
);
|
|
}}
|
|
onMouseLeave={goBackToPreviousHotkeyScope}
|
|
>
|
|
<DropdownMenuItemsContainer>
|
|
<StyledMenuIconItemsContainer>
|
|
{matchingSearchIconKeys.map((iconKey) => (
|
|
<IconPickerIcon
|
|
key={iconKey}
|
|
iconKey={iconKey}
|
|
onClick={() => {
|
|
onChange({ iconKey, Icon: getIcon(iconKey) });
|
|
closeDropdown();
|
|
}}
|
|
selectedIconKey={selectedIconKey}
|
|
Icon={getIcon(iconKey)}
|
|
/>
|
|
))}
|
|
</StyledMenuIconItemsContainer>
|
|
</DropdownMenuItemsContainer>
|
|
</div>
|
|
</DropdownMenu>
|
|
</SelectableList>
|
|
}
|
|
onClickOutside={onClickOutside}
|
|
onClose={() => {
|
|
onClose?.();
|
|
setSearchString('');
|
|
}}
|
|
onOpen={onOpen}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|