diff --git a/src/components/Avatar/Avatar.props.ts b/src/components/Avatar/Avatar.props.ts index 7c4f6ec7..2031a4ee 100644 --- a/src/components/Avatar/Avatar.props.ts +++ b/src/components/Avatar/Avatar.props.ts @@ -8,20 +8,21 @@ import type { AvatarSize, } from './types'; -export type AvatarProps = ( - | { - img: Omit, 'color'>; - appearance: 'image'; - emphasis?: 'low'; - initials?: never; - } - | { - img?: never; - appearance?: AvatarAppearanceColors; - emphasis?: AvatarEmphasis; - initials: string; - } -) & { +export type AvatarImageProps = { + img: Omit, 'color'>; + appearance: 'image'; + emphasis?: 'low'; + initials?: never; +}; + +export type AvatarInitialProps = { + img?: never; + appearance?: AvatarAppearanceColors; + emphasis?: AvatarEmphasis; + initials: string; +}; + +export type AvatarProps = (AvatarImageProps | AvatarInitialProps) & { shape?: AvatarShape; size?: AvatarSize; custom?: AvatarConfig; diff --git a/src/components/IconButton/IconButton.tsx b/src/components/IconButton/IconButton.tsx index 32eeda8d..a83976ad 100644 --- a/src/components/IconButton/IconButton.tsx +++ b/src/components/IconButton/IconButton.tsx @@ -1,5 +1,4 @@ import { Icon } from '@virtuslab/tetrisly-icons'; -import { MarginProps } from '@xstyled/styled-components'; import { useMemo } from 'react'; import { IconButtonProps } from './IconButton.props'; @@ -9,6 +8,7 @@ import { ButtonVariant } from '../Button/types/ButtonType.type'; import { Loader } from '../Loader'; import { tet } from '@/tetrisly'; +import { MarginProps } from '@/types'; export const IconButton = < TVariant extends ButtonVariant, diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index 94049644..2f0fe253 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -1,4 +1,3 @@ -import { MarginProps } from '@xstyled/styled-components'; import { FC, useMemo } from 'react'; import { AnimatedProgress } from './AnimatedProgress'; @@ -6,6 +5,7 @@ import { LoaderProps } from './Loader.props'; import { stylesBuilder } from './stylesBuilder'; import { tet } from '@/tetrisly'; +import { MarginProps } from '@/types'; export const Loader: FC = ({ appearance = 'primary', diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index 7d1e0a7f..3c136fdb 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -1,9 +1,10 @@ -import { MarginProps } from '@xstyled/styled-components'; import { FC } from 'react'; import { SearchInputProps } from './SearchInput.props'; import { TextInput, TextInputProps } from '../TextInput'; +import { MarginProps } from '@/types'; + const SEARCH_ICON_COMPONENT: TextInputProps['beforeComponent'] = { type: 'Icon', props: { diff --git a/src/components/Select/Select.props.ts b/src/components/Select/Select.props.ts new file mode 100644 index 00000000..5e7a0ac3 --- /dev/null +++ b/src/components/Select/Select.props.ts @@ -0,0 +1,10 @@ +import type { TextInputProps } from '../TextInput'; + +export type SelectProps = Omit< + TextInputProps, + 'beforeComponent' | 'afterComponent' | 'type' +> & { + beforeComponent?: + | TextInputProps.InnerComponents.Icon + | TextInputProps.InnerComponents.Avatar; +}; diff --git a/src/components/Select/Select.stories.tsx b/src/components/Select/Select.stories.tsx new file mode 100644 index 00000000..f5509c1a --- /dev/null +++ b/src/components/Select/Select.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Select } from './Select'; + +import { TetDocs } from '@/docs-components/TetDocs'; + +const meta = { + component: Select, + tags: ['autodocs'], + args: { + placeholder: 'Input placeholder', + }, + argTypes: {}, + + parameters: { + docs: { + description: { + component: + 'A component that allows users to choose one or more options from a list, typically presented as a dropdown or pop-up menu. Select components are commonly used in forms and settings.', + }, + page: () => ( + + ), + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Disabled: Story = { + args: { + state: 'disabled', + }, +}; + +export const Alert: Story = { + args: { + state: 'alert', + }, +}; + +export const BeforeIconComponent: Story = { + args: { + beforeComponent: { + type: 'Icon', + props: { + name: '20-bolt', + }, + }, + }, +}; + +export const BeforeAvatarComponent: Story = { + args: { + beforeComponent: { + type: 'Avatar', + props: { + initials: 'A', + }, + }, + }, +}; diff --git a/src/components/Select/Select.test.tsx b/src/components/Select/Select.test.tsx new file mode 100644 index 00000000..1c7b40ee --- /dev/null +++ b/src/components/Select/Select.test.tsx @@ -0,0 +1,73 @@ +import { vi } from 'vitest'; + +import { Select } from './Select'; +import { fireEvent, render } from '../../tests/render'; + +const handleEventMock = vi.fn(); + +const getSelect = (jsx: JSX.Element) => { + const { getByTestId, queryByTestId } = render(jsx); + + return { + textInput: getByTestId('text-input'), + input: getByTestId('text-input-input') as HTMLInputElement, + beforeComponent: queryByTestId('text-input-before-component'), + }; +}; + +describe('Select', () => { + beforeEach(() => { + handleEventMock.mockClear(); + }); + + it('should render the select', () => { + const { textInput } = getSelect(); + + if (input) { + fireEvent.change(input, { target: { value: '2020-05-24' } }); + } + + expect(handleEventMock).toHaveBeenCalled(); + }); + + it('should emit onBlur', () => { + const { input } = getSelect(); + + if (input) { + fireEvent.focus(input); + } + + expect(handleEventMock).toHaveBeenCalled(); + }); + + it('should render beforeComponent', () => { + const { beforeComponent } = getSelect( + , + ); + + expect(textInput).toHaveStyle('background-color: rgb(254, 245, 245)'); + }); +}); diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx new file mode 100644 index 00000000..395c12fb --- /dev/null +++ b/src/components/Select/Select.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; + +import { SelectProps } from './Select.props'; +import { TextInput, TextInputProps } from '../TextInput'; + +import { MarginProps } from '@/types'; + +const DROPDOWN_INDICATOR_COMPONENT: TextInputProps['afterComponent'] = { + type: 'IconButton', + props: { + icon: '20-chevron-down-small', + }, +}; + +export const Select: FC = (props) => ( + +); diff --git a/src/components/Select/index.ts b/src/components/Select/index.ts new file mode 100644 index 00000000..57890d63 --- /dev/null +++ b/src/components/Select/index.ts @@ -0,0 +1,2 @@ +export { Select } from './Select'; +export type { SelectProps } from './Select.props'; diff --git a/src/components/TextInput/TextInput.props.ts b/src/components/TextInput/TextInput.props.ts index 2c149e36..f669d2f9 100644 --- a/src/components/TextInput/TextInput.props.ts +++ b/src/components/TextInput/TextInput.props.ts @@ -3,6 +3,7 @@ import { InputHTMLAttributes } from 'react'; import { TextInputConfig } from './TextInput.style'; import { TextInputType } from './TextInputType.type'; +import { AvatarImageProps, AvatarInitialProps } from '../Avatar/Avatar.props'; import { ButtonProps } from '../Button'; import { IconButtonProps } from '../IconButton/IconButton.props'; @@ -25,12 +26,17 @@ export namespace TextInputProps.InnerComponents { 'Button', Pick, 'label' | 'onClick'> >; + export type Avatar = InnerComponent< + 'Avatar', + AvatarImageProps | AvatarInitialProps + >; } export type TextInputProps = { type?: TextInputType; beforeComponent?: | TextInputProps.InnerComponents.Icon + | TextInputProps.InnerComponents.Avatar | TextInputProps.InnerComponents.Prefix | TextInputProps.InnerComponents.Dropdown; afterComponent?: diff --git a/src/components/TextInput/TextInput.stories.tsx b/src/components/TextInput/TextInput.stories.tsx index 88c6d7ba..2b519d6f 100644 --- a/src/components/TextInput/TextInput.stories.tsx +++ b/src/components/TextInput/TextInput.stories.tsx @@ -45,6 +45,17 @@ export const BeforeIconComponent: Story = { }, }; +export const BeforeAvatarComponent: Story = { + args: { + beforeComponent: { + type: 'Avatar', + props: { + initials: 'A', + }, + }, + }, +}; + export const BeforePrefixComponent: Story = { args: { beforeComponent: { diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx index 42d45dd0..2ba6514d 100644 --- a/src/components/TextInput/TextInput.tsx +++ b/src/components/TextInput/TextInput.tsx @@ -12,6 +12,7 @@ import { import { stylesBuilder } from './stylesBuilder'; import { TextInputProps } from './TextInput.props'; +import { Avatar } from '../Avatar'; import { Button } from '../Button'; import { IconButton } from '../IconButton'; @@ -105,6 +106,9 @@ export const TextInput = forwardRef< dropdownIndicator /> )} + {beforeComponent.type === 'Avatar' && ( + + )} )}