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

@ -0,0 +1,3 @@
<svg width="34" height="16" viewBox="0 0 34 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7C0.447715 7 0 7.44772 0 8C0 8.55228 0.447715 9 1 9V7ZM33.7071 8.70711C34.0976 8.31658 34.0976 7.68342 33.7071 7.29289L27.3431 0.928932C26.9526 0.538408 26.3195 0.538408 25.9289 0.928932C25.5384 1.31946 25.5384 1.95262 25.9289 2.34315L31.5858 8L25.9289 13.6569C25.5384 14.0474 25.5384 14.6805 25.9289 15.0711C26.3195 15.4616 26.9526 15.4616 27.3431 15.0711L33.7071 8.70711ZM1 9H33V7H1V9Z" fill="#EBEBEB"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@ -0,0 +1,42 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
type IconWithLabelProps = {
Icon: IconComponent;
label: string;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(3)};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledSubContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledItemLabel = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
font-style: normal;
font-weight: ${({ theme }) => theme.font.size.md};
line-height: ${({ theme }) => theme.text.lineHeight.md};
`;
export const IconWithLabel = ({ Icon, label }: IconWithLabelProps) => {
const theme = useTheme();
return (
<StyledContainer>
<StyledSubContainer>
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
<StyledItemLabel>{label}</StyledItemLabel>
</StyledSubContainer>
</StyledContainer>
);
};

View File

@ -0,0 +1,55 @@
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { H2Title } from '@/ui/typography/components/H2Title';
import ArrowRight from '../assets/ArrowRight.svg';
import { IconWithLabel } from './IconWithLabel';
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledArrowContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
`;
type SettingsIconSectionProps = {
Icon: IconComponent;
iconKey: string;
setIconPicker: (icon: { Icon: IconComponent; iconKey: string }) => void;
};
export const SettingsIconSection = ({
Icon,
iconKey,
setIconPicker,
}: SettingsIconSectionProps) => {
return (
<section>
<H2Title
title="Icon"
description="The icon that will be displayed in the sidebar."
/>
<StyledContainer>
<IconPicker
selectedIconKey={iconKey}
onChange={(icon) => {
setIconPicker({ Icon: icon.Icon, iconKey: icon.iconKey });
}}
/>
<StyledArrowContainer>
<img src={ArrowRight} alt="Arrow right" width={32} height={16} />
</StyledArrowContainer>
<IconWithLabel Icon={Icon} label="Workspaces" />
</StyledContainer>
</section>
);
};

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('');
},
};

View File

@ -0,0 +1,13 @@
import { useRecoilState } from 'recoil';
import { iconPickerState } from '../states/iconPickerState';
export const useIconPicker = () => {
const [iconPicker, setIconPicker] = useRecoilState(iconPickerState);
return {
Icon: iconPicker.Icon,
iconKey: iconPicker.iconKey,
setIconPicker,
};
};

View File

@ -0,0 +1,15 @@
import { atom } from 'recoil';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { IconApps } from '../constants/icons';
type IconPickerState = {
Icon: IconComponent;
iconKey: string;
};
export const iconPickerState = atom<IconPickerState>({
key: 'iconPickerState',
default: { Icon: IconApps, iconKey: 'IconApps' },
});

View File

@ -0,0 +1,3 @@
export enum IconPickerHotkeyScope {
IconPicker = 'icon-picker',
}

View File

@ -1,25 +1,41 @@
import styled from '@emotion/styled';
import { SettingsIconSection } from '@/settings/components/SettingsIconSection';
import { objectSettingsWidth } from '@/settings/objects/constants/objectSettings';
import { Breadcrumb } from '@/ui/breadcrumb/components/Breadcrumb';
import { IconSettings } from '@/ui/icon';
import { useIconPicker } from '@/ui/input/hooks/useIconPicker';
import { SubMenuTopBarContainer } from '@/ui/layout/components/SubMenuTopBarContainer';
const StyledContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
height: fit-content;
padding: ${({ theme }) => theme.spacing(8)};
width: ${objectSettingsWidth};
`;
export const SettingsNewObject = () => (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<StyledContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{ children: 'New' },
]}
/>
</StyledContainer>
</SubMenuTopBarContainer>
);
export const SettingsNewObject = () => {
const { Icon, iconKey, setIconPicker } = useIconPicker();
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<StyledContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{ children: 'New' },
]}
/>
<SettingsIconSection
Icon={Icon}
iconKey={iconKey}
setIconPicker={setIconPicker}
/>
</StyledContainer>
</SubMenuTopBarContainer>
);
};