From d9feabbc63df4bf8923e02c53487a4c63a13767a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Wed, 27 Sep 2023 17:56:49 +0200 Subject: [PATCH] feat: add IconPicker (#1730) * feat: add IconPicker Closes #1657 * fix: fix front lint errors * refactor: rename selectedIconName to selectedIconKey --- .../ui/button/components/LightIconButton.tsx | 6 +- .../ui/input/components/IconPicker.tsx | 80 +++++++++++++++++++ .../__stories__/IconPicker.stories.tsx | 47 +++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 front/src/modules/ui/input/components/IconPicker.tsx create mode 100644 front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx diff --git a/front/src/modules/ui/button/components/LightIconButton.tsx b/front/src/modules/ui/button/components/LightIconButton.tsx index e8d8aea14..3f5e10c1b 100644 --- a/front/src/modules/ui/button/components/LightIconButton.tsx +++ b/front/src/modules/ui/button/components/LightIconButton.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent } from 'react'; +import { ComponentProps, MouseEvent } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -17,7 +17,7 @@ export type LightIconButtonProps = { disabled?: boolean; focus?: boolean; onClick?: (event: MouseEvent) => void; -}; +} & Pick, 'aria-label'>; const StyledButton = styled.button< Pick @@ -79,6 +79,7 @@ const StyledButton = styled.button< `; export const LightIconButton = ({ + 'aria-label': ariaLabel, className, Icon, active = false, @@ -91,6 +92,7 @@ export const LightIconButton = ({ const theme = useTheme(); return ( ; + onChange: (iconName: string) => void; + selectedIconKey?: string; +}; + +const StyledIconPickerDropdownMenu = styled(StyledDropdownMenu)` + width: 176px; +`; + +const StyledMenuIconItemsContainer = styled(StyledDropdownMenuItemsContainer)` + flex-direction: row; + flex-wrap: wrap; + height: auto; +`; + +const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>` + background: ${({ theme, isSelected }) => + isSelected ? theme.background.transparent.light : 'transparent'}; +`; + +const convertIconKeyToLabel = (iconName: string) => + iconName.replace(/[A-Z]/g, (letter) => ` ${letter}`).trim(); + +export const IconPicker = ({ + icons, + onChange, + selectedIconKey, +}: IconPickerProps) => { + const [searchString, setSearchString] = useState(''); + + const iconKeys = useMemo(() => { + const filteredIconKeys = Object.keys(icons).filter( + (iconKey) => + iconKey !== selectedIconKey && + (!searchString || + convertIconKeyToLabel(iconKey) + .toLowerCase() + .includes(searchString.toLowerCase())), + ); + + return ( + selectedIconKey + ? [selectedIconKey, ...filteredIconKeys] + : filteredIconKeys + ).slice(0, 25); + }, [icons, searchString, selectedIconKey]); + + return ( + + setSearchString(event.target.value)} + /> + + + {iconKeys.map((iconKey) => ( + onChange(iconKey)} + /> + ))} + + + ); +}; diff --git a/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx b/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx new file mode 100644 index 000000000..ff8b4a49d --- /dev/null +++ b/front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx @@ -0,0 +1,47 @@ +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import * as icons from '@/ui/icon'; +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { IconPicker } from '../IconPicker'; + +const meta: Meta = { + title: 'UI/Input/IconPicker', + component: IconPicker, + decorators: [ComponentDecorator], + args: { icons }, + argTypes: { + icons: { control: false }, + selectedIconKey: { + options: Object.keys(icons), + control: { type: 'select' }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithSelectedIcon: Story = { + args: { selectedIconKey: 'IconCalendarEvent' }, +}; + +export const WithSearch: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const searchInput = canvas.getByRole('textbox'); + + await userEvent.type(searchInput, 'Building skyscraper'); + + const searchedIcon = canvas.getByRole('button', { + name: 'Icon Building Skyscraper', + }); + + expect(searchedIcon).toBeInTheDocument(); + }, +};