From 30aeea9eec09ac08771c1c526234bb3ae69c0029 Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:29:30 +0200 Subject: [PATCH] 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 --- .../modules/settings/assets/ArrowRight.svg | 3 + .../settings/components/IconWithLabel.tsx | 42 +++++++++ .../components/SettingsIconSection.tsx | 55 +++++++++++ .../ui/input/components/IconPicker.tsx | 94 +++++++++++++------ .../__stories__/IconPicker.stories.tsx | 63 ++++++++++++- .../modules/ui/input/hooks/useIconPicker.ts | 13 +++ .../ui/input/states/iconPickerState.ts | 15 +++ .../ui/input/types/IconPickerHotkeyScope.ts | 3 + .../src/pages/settings/SettingsNewObject.tsx | 40 +++++--- 9 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 front/src/modules/settings/assets/ArrowRight.svg create mode 100644 front/src/modules/settings/components/IconWithLabel.tsx create mode 100644 front/src/modules/settings/components/SettingsIconSection.tsx create mode 100644 front/src/modules/ui/input/hooks/useIconPicker.ts create mode 100644 front/src/modules/ui/input/states/iconPickerState.ts create mode 100644 front/src/modules/ui/input/types/IconPickerHotkeyScope.ts diff --git a/front/src/modules/settings/assets/ArrowRight.svg b/front/src/modules/settings/assets/ArrowRight.svg new file mode 100644 index 000000000..28249ab56 --- /dev/null +++ b/front/src/modules/settings/assets/ArrowRight.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/front/src/modules/settings/components/IconWithLabel.tsx b/front/src/modules/settings/components/IconWithLabel.tsx new file mode 100644 index 000000000..1470b32dc --- /dev/null +++ b/front/src/modules/settings/components/IconWithLabel.tsx @@ -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 ( + + + + {label} + + + ); +}; diff --git a/front/src/modules/settings/components/SettingsIconSection.tsx b/front/src/modules/settings/components/SettingsIconSection.tsx new file mode 100644 index 000000000..6224032da --- /dev/null +++ b/front/src/modules/settings/components/SettingsIconSection.tsx @@ -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 ( +
+ + + { + setIconPicker({ Icon: icon.Icon, iconKey: icon.iconKey }); + }} + /> + + Arrow right + + + +
+ ); +}; diff --git a/front/src/modules/ui/input/components/IconPicker.tsx b/front/src/modules/ui/input/components/IconPicker.tsx index 860739bd0..8b771356c 100644 --- a/front/src/modules/ui/input/components/IconPicker.tsx +++ b/front/src/modules/ui/input/components/IconPicker.tsx @@ -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>({}); + 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 ( - - setSearchString(event.target.value)} - /> - - - {isLoading ? ( - - ) : ( - iconKeys.map((iconKey) => ( - onChange({ iconKey, Icon: icons[iconKey] })} + + + } + dropdownComponents={ + + setSearchString(event.target.value)} /> - )) - )} - - + + + {isLoading ? ( + + ) : ( + + {iconKeys.map((iconKey) => ( + { + onChange({ iconKey, Icon: icons[iconKey] }); + closeDropdown(); + }} + /> + ))} + + )} + + + } + onClickOutside={onClickOutside} + onClose={() => { + onClose?.(); + setSearchString(''); + }} + onOpen={onOpen} + > + ); }; diff --git a/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx b/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx index be7d26d51..db8041c72 100644 --- a/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx +++ b/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx @@ -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(''); + }, +}; diff --git a/front/src/modules/ui/input/hooks/useIconPicker.ts b/front/src/modules/ui/input/hooks/useIconPicker.ts new file mode 100644 index 000000000..141431c71 --- /dev/null +++ b/front/src/modules/ui/input/hooks/useIconPicker.ts @@ -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, + }; +}; diff --git a/front/src/modules/ui/input/states/iconPickerState.ts b/front/src/modules/ui/input/states/iconPickerState.ts new file mode 100644 index 000000000..0aa9c3dec --- /dev/null +++ b/front/src/modules/ui/input/states/iconPickerState.ts @@ -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({ + key: 'iconPickerState', + default: { Icon: IconApps, iconKey: 'IconApps' }, +}); diff --git a/front/src/modules/ui/input/types/IconPickerHotkeyScope.ts b/front/src/modules/ui/input/types/IconPickerHotkeyScope.ts new file mode 100644 index 000000000..f38981837 --- /dev/null +++ b/front/src/modules/ui/input/types/IconPickerHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum IconPickerHotkeyScope { + IconPicker = 'icon-picker', +} diff --git a/front/src/pages/settings/SettingsNewObject.tsx b/front/src/pages/settings/SettingsNewObject.tsx index e1513c4a0..3655e62e4 100644 --- a/front/src/pages/settings/SettingsNewObject.tsx +++ b/front/src/pages/settings/SettingsNewObject.tsx @@ -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 = () => ( - - - - - -); +export const SettingsNewObject = () => { + const { Icon, iconKey, setIconPicker } = useIconPicker(); + + return ( + + + + + + + + ); +};