1909 object edit add icon section (#1995)

* wip

* wip

* wip

* wip

* wip

* remove hardcoded values and use theme values

* add styles to StyledContainer

* fix iconPicker bug

* wip

* refactor IconPicker to include IconButton

* close IconPicker on click outside

* close IconPicker on escape and enter

* refactor to use DropDownMenu

* refactor to use DropDownMenu

* modify default icon

* Refactor to use useIconPicker hook

* fix WithSearch story

* reinitialized searchString state on close

* create and update stories for the iconPicker

* remove comments

* use theme for gap

* remove align-self

* fix typo in icon

* fix type any

* fix merge conflicts

* remove experimental css properties
This commit is contained in:
bosiraphael
2023-10-13 15:29:30 +02:00
committed by GitHub
parent 818efd72d0
commit 30aeea9eec
9 changed files with 287 additions and 41 deletions

View File

@ -1,28 +1,36 @@
import { useEffect, useMemo, useState } from 'react';
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdown } from '@/ui/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/dropdown/scopes/DropdownScope';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { IconApps } from '../constants/icons';
import { DropdownMenuSkeletonItem } from '../relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
type IconPickerProps = {
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
selectedIconKey?: string;
onClickOutside?: () => void;
onClose?: () => void;
onOpen?: () => void;
};
const StyledIconPickerDropdownMenu = styled(StyledDropdownMenu)`
const StyledContainer = styled.div`
width: 176px;
`;
const StyledMenuIconItemsContainer = styled(DropdownMenuItemsContainer)`
const StyledMenuIconItemsContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: auto;
`;
const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
@ -33,11 +41,19 @@ const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
const convertIconKeyToLabel = (iconKey: string) =>
iconKey.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim();
export const IconPicker = ({ onChange, selectedIconKey }: IconPickerProps) => {
export const IconPicker = ({
onChange,
selectedIconKey,
onClickOutside,
onClose,
onOpen,
}: IconPickerProps) => {
const [searchString, setSearchString] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [icons, setIcons] = useState<Record<string, IconComponent>>({});
const { closeDropdown } = useDropdown({ dropdownScopeId: 'icon-picker' });
useEffect(() => {
import('../constants/icons').then((lazyLoadedIcons) => {
setIcons(lazyLoadedIcons);
@ -63,28 +79,52 @@ export const IconPicker = ({ onChange, selectedIconKey }: IconPickerProps) => {
}, [icons, searchString, selectedIconKey]);
return (
<StyledIconPickerDropdownMenu>
<DropdownMenuSearchInput
placeholder="Search icon"
autoFocus
onChange={(event) => setSearchString(event.target.value)}
/>
<StyledDropdownMenuSeparator />
<StyledMenuIconItemsContainer>
{isLoading ? (
<DropdownMenuSkeletonItem />
) : (
iconKeys.map((iconKey) => (
<StyledLightIconButton
aria-label={convertIconKeyToLabel(iconKey)}
isSelected={selectedIconKey === iconKey}
size="medium"
Icon={icons[iconKey]}
onClick={() => onChange({ iconKey, Icon: icons[iconKey] })}
<DropdownScope dropdownScopeId="icon-picker">
<DropdownMenu
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
clickableComponent={
<IconButton
Icon={selectedIconKey ? icons[selectedIconKey] : IconApps}
variant="secondary"
/>
}
dropdownComponents={
<StyledContainer>
<DropdownMenuSearchInput
placeholder="Search icon"
autoFocus
onChange={(event) => setSearchString(event.target.value)}
/>
))
)}
</StyledMenuIconItemsContainer>
</StyledIconPickerDropdownMenu>
<StyledDropdownMenuSeparator />
<DropdownMenuItemsContainer>
{isLoading ? (
<DropdownMenuSkeletonItem />
) : (
<StyledMenuIconItemsContainer>
{iconKeys.map((iconKey) => (
<StyledLightIconButton
aria-label={convertIconKeyToLabel(iconKey)}
isSelected={selectedIconKey === iconKey}
size="medium"
Icon={icons[iconKey]}
onClick={() => {
onChange({ iconKey, Icon: icons[iconKey] });
closeDropdown();
}}
/>
))}
</StyledMenuIconItemsContainer>
)}
</DropdownMenuItemsContainer>
</StyledContainer>
}
onClickOutside={onClickOutside}
onClose={() => {
onClose?.();
setSearchString('');
}}
onOpen={onOpen}
></DropdownMenu>
</DropdownScope>
);
};

View File

@ -22,15 +22,40 @@ export const WithSelectedIcon: Story = {
args: { selectedIconKey: 'IconCalendarEvent' },
};
export const WithOpen: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const iconPickerButton = await canvas.findByRole('button');
userEvent.click(iconPickerButton);
},
};
export const WithOpenAndSelectedIcon: Story = {
args: { selectedIconKey: 'IconCalendarEvent' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const iconPickerButton = await canvas.findByRole('button');
userEvent.click(iconPickerButton);
},
};
export const WithSearch: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const searchInput = canvas.getByRole('textbox');
const iconPickerButton = await canvas.findByRole('button');
userEvent.click(iconPickerButton);
const searchInput = await canvas.findByRole('textbox');
await userEvent.type(searchInput, 'Building skyscraper');
await sleep(1000);
await sleep(100);
const searchedIcon = canvas.getByRole('button', {
name: 'Icon Building Skyscraper',
@ -39,3 +64,37 @@ export const WithSearch: Story = {
expect(searchedIcon).toBeInTheDocument();
},
};
export const WithSearchAndClose: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const iconPickerButton = await canvas.findByRole('button');
userEvent.click(iconPickerButton);
let searchInput = await canvas.findByRole('textbox');
await userEvent.type(searchInput, 'Building skyscraper');
await sleep(100);
const searchedIcon = canvas.getByRole('button', {
name: 'Icon Building Skyscraper',
});
expect(searchedIcon).toBeInTheDocument();
userEvent.click(searchedIcon);
await sleep(100);
userEvent.click(iconPickerButton);
await sleep(100);
searchInput = await canvas.findByRole('textbox');
expect(searchInput).toHaveValue('');
},
};