feat: add IconPicker (#1730)

* feat: add IconPicker

Closes #1657

* fix: fix front lint errors

* refactor: rename selectedIconName to selectedIconKey
This commit is contained in:
Thaïs
2023-09-27 17:56:49 +02:00
committed by GitHub
parent 46ad36061e
commit d9feabbc63
3 changed files with 131 additions and 2 deletions

View File

@ -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<HTMLButtonElement>) => void;
};
} & Pick<ComponentProps<'button'>, 'aria-label'>;
const StyledButton = styled.button<
Pick<LightIconButtonProps, 'accent' | 'active' | 'size' | 'focus'>
@ -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 (
<StyledButton
aria-label={ariaLabel}
onClick={onClick}
disabled={disabled}
focus={focus && !disabled}

View File

@ -0,0 +1,80 @@
import { useMemo, useState } from 'react';
import styled from '@emotion/styled';
import { LightIconButton } from '@/ui/button/components/LightIconButton';
import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconComponent } from '@/ui/icon/types/IconComponent';
type IconPickerProps = {
icons: Record<string, IconComponent>;
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 (
<StyledIconPickerDropdownMenu>
<DropdownMenuSearchInput
placeholder="Search icon"
autoFocus
onChange={(event) => setSearchString(event.target.value)}
/>
<StyledDropdownMenuSeparator />
<StyledMenuIconItemsContainer>
{iconKeys.map((iconKey) => (
<StyledLightIconButton
aria-label={convertIconKeyToLabel(iconKey)}
isSelected={selectedIconKey === iconKey}
size="medium"
Icon={icons[iconKey]}
onClick={() => onChange(iconKey)}
/>
))}
</StyledMenuIconItemsContainer>
</StyledIconPickerDropdownMenu>
);
};

View File

@ -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<typeof IconPicker> = {
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<typeof IconPicker>;
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();
},
};