Skip to content

Commit

Permalink
feat: TET-141 select (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
mateusz-kleszcz authored Sep 14, 2023
1 parent 90c3d4d commit 65e1f27
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 17 deletions.
29 changes: 15 additions & 14 deletions src/components/Avatar/Avatar.props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ import type {
AvatarSize,
} from './types';

export type AvatarProps = (
| {
img: Omit<ImgHTMLAttributes<HTMLImageElement>, 'color'>;
appearance: 'image';
emphasis?: 'low';
initials?: never;
}
| {
img?: never;
appearance?: AvatarAppearanceColors;
emphasis?: AvatarEmphasis;
initials: string;
}
) & {
export type AvatarImageProps = {
img: Omit<ImgHTMLAttributes<HTMLImageElement>, '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;
Expand Down
2 changes: 1 addition & 1 deletion src/components/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { MarginProps } from '@xstyled/styled-components';
import { FC, useMemo } from 'react';

import { AnimatedProgress } from './AnimatedProgress';
import { LoaderProps } from './Loader.props';
import { stylesBuilder } from './stylesBuilder';

import { tet } from '@/tetrisly';
import { MarginProps } from '@/types';

export const Loader: FC<LoaderProps & MarginProps> = ({
appearance = 'primary',
Expand Down
3 changes: 2 additions & 1 deletion src/components/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
10 changes: 10 additions & 0 deletions src/components/Select/Select.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { TextInputProps } from '../TextInput';

export type SelectProps = Omit<
TextInputProps,
'beforeComponent' | 'afterComponent' | 'type'
> & {
beforeComponent?:
| TextInputProps.InnerComponents.Icon
| TextInputProps.InnerComponents.Avatar;
};
65 changes: 65 additions & 0 deletions src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -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: () => (
<TetDocs docs="https://docs.tetrisly.com/components/list/select" />
),
},
},
} satisfies Meta<typeof Select>;

export default meta;
type Story = StoryObj<typeof meta>;

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',
},
},
},
};
73 changes: 73 additions & 0 deletions src/components/Select/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Select />);
expect(textInput).toBeInTheDocument();
});

it('should emit onChange', () => {
const { input } = getSelect(<Select value="" onChange={handleEventMock} />);

if (input) {
fireEvent.change(input, { target: { value: '2020-05-24' } });
}

expect(handleEventMock).toHaveBeenCalled();
});

it('should emit onBlur', () => {
const { input } = getSelect(<Select onBlur={handleEventMock} />);

if (input) {
fireEvent.blur(input);
}

expect(handleEventMock).toHaveBeenCalled();
});

it('should emit onFocus', () => {
const { input } = getSelect(<Select onFocus={handleEventMock} />);

if (input) {
fireEvent.focus(input);
}

expect(handleEventMock).toHaveBeenCalled();
});

it('should render beforeComponent', () => {
const { beforeComponent } = getSelect(
<Select beforeComponent={{ type: 'Icon', props: { name: '20-bolt' } }} />,
);

expect(beforeComponent).toBeInTheDocument();
});

it('should propagate custom prop to text input', () => {
const { textInput } = getSelect(
<Select custom={{ backgroundColor: 'background-negative-subtle' }} />,
);

expect(textInput).toHaveStyle('background-color: rgb(254, 245, 245)');
});
});
17 changes: 17 additions & 0 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -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<SelectProps & MarginProps> = (props) => (
<TextInput afterComponent={DROPDOWN_INDICATOR_COMPONENT} {...props} />
);
2 changes: 2 additions & 0 deletions src/components/Select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Select } from './Select';
export type { SelectProps } from './Select.props';
6 changes: 6 additions & 0 deletions src/components/TextInput/TextInput.props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -25,12 +26,17 @@ export namespace TextInputProps.InnerComponents {
'Button',
Pick<ButtonProps<'ghost'>, '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?:
Expand Down
11 changes: 11 additions & 0 deletions src/components/TextInput/TextInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions src/components/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -105,6 +106,9 @@ export const TextInput = forwardRef<
dropdownIndicator
/>
)}
{beforeComponent.type === 'Avatar' && (
<Avatar {...beforeComponent.props} shape="square" size="xSmall" />
)}
</tet.span>
)}
<tet.input
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './components/Popover';
export * from './components/RadioButton';
export * from './components/RadioButtonGroup';
export * from './components/SearchInput';
export * from './components/Select';
export * from './components/StatusDot';
export * from './components/TextInput';
export * from './components/InlineSearchInput';
Expand Down

0 comments on commit 65e1f27

Please sign in to comment.