Skip to content

Commit 3fab564

Browse files
Elena Rashkovanlena.rashkovan
andauthored
feat: adds input component (#538)
* feat(input): add experimental component * feat(search): use input component * fix(input): adjust flex props --------- Co-authored-by: lena.rashkovan <[email protected]>
1 parent 5718f31 commit 3fab564

File tree

6 files changed

+128
-49
lines changed

6 files changed

+128
-49
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
5+
import { Input } from './Input';
6+
7+
describe('Experimental: Input', () => {
8+
it('renders correctly', async () => {
9+
const user = userEvent.setup();
10+
const utils = render(<Input placeholder="Test" />);
11+
12+
const input = await utils.findByPlaceholderText('Test');
13+
14+
await user.type(input, 'Text');
15+
16+
expect(input).toHaveValue('Text');
17+
});
18+
});

src/components/experimental/Search/Search.styled.ts renamed to src/components/experimental/Input/Input.styled.ts

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1-
import { Input as BaseInput, Button as BaseButton, SearchField as BaseSearchField } from 'react-aria-components';
1+
import { Input as BaseInput } from 'react-aria-components';
22
import styled from 'styled-components';
33

44
import { getSemanticValue } from '../../../essentials/experimental';
5-
import SearchIcon from '../../../icons/experimental/SearchIcon';
65
import { get } from '../../../utils/experimental/themeGet';
76
import { textStyles } from '../Text/Text';
87

9-
export const Icon = styled(SearchIcon)`
10-
position: absolute;
11-
left: ${get('space.3')};
12-
top: 50%;
13-
transform: translateY(-50%);
14-
pointer-events: none;
15-
`;
16-
17-
export const SearchField = styled(BaseSearchField)`
8+
export const Wrapper = styled.span`
189
position: relative;
19-
border-radius: ${get('radii.4')};
10+
11+
display: flex;
12+
align-items: center;
13+
gap: ${get('space.1')};
14+
padding: ${get('space.2')} ${get('space.4')};
15+
2016
background: ${getSemanticValue('surface-variant')};
2117
color: ${getSemanticValue('on-surface-variant')};
18+
border-radius: ${get('radii.4')};
19+
20+
cursor: text;
2221
23-
&::before {
22+
& > * {
23+
flex-shrink: 0;
24+
}
25+
26+
&::after {
2427
position: absolute;
2528
pointer-events: none;
2629
inset: 0;
@@ -30,9 +33,9 @@ export const SearchField = styled(BaseSearchField)`
3033
transition: opacity ease 200ms;
3134
}
3235
33-
&:has([data-hovered])::before {
36+
&:hover::after {
3437
opacity: 0.16;
35-
background-color: ${getSemanticValue('on-surface-variant')};
38+
background: ${getSemanticValue('on-surface')};
3639
}
3740
3841
&:has([data-focused]) {
@@ -41,43 +44,31 @@ export const SearchField = styled(BaseSearchField)`
4144
outline: ${getSemanticValue('interactive')} solid 0.125rem;
4245
outline-offset: -0.125rem;
4346
44-
&::before {
47+
&::after {
4548
opacity: 0;
4649
}
4750
}
4851
4952
&:has([data-disabled]) {
5053
opacity: 0.38;
54+
pointer-events: none;
5155
}
5256
`;
5357

54-
export const Button = styled(BaseButton)`
55-
appearance: none;
56-
background: none;
57-
display: flex;
58-
margin: 0;
59-
padding: 0;
58+
export const Input = styled(BaseInput)`
6059
border: 0;
6160
outline: 0;
62-
cursor: pointer;
63-
position: absolute;
64-
right: ${get('space.3')};
65-
top: 50%;
66-
transform: translateY(-50%);
67-
`;
61+
padding: 0;
62+
63+
flex-grow: 1;
6864
69-
export const Input = styled(BaseInput)`
7065
background-color: unset;
71-
display: block;
72-
padding: ${get('space.2')} ${get('space.9')};
73-
border: 0;
74-
outline: 0;
75-
caret-color: ${getSemanticValue('interactive')};
7666
color: ${getSemanticValue('on-surface')};
67+
caret-color: ${getSemanticValue('interactive')};
7768
7869
${textStyles.variants.label1}
7970
80-
&[data-placeholder] {
71+
&::placeholder {
8172
color: ${getSemanticValue('on-surface-variant')};
8273
}
8374
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { ReactElement } from 'react';
2+
import { InputProps as BaseInputProps } from 'react-aria-components';
3+
4+
import * as Styled from './Input.styled';
5+
6+
interface InputProps extends BaseInputProps {
7+
leadingIcon?: React.ReactNode;
8+
actionIcon?: React.ReactNode;
9+
}
10+
11+
export const Input = ({ leadingIcon, actionIcon, ...rest }: InputProps): ReactElement => {
12+
const inputRef = React.useRef<HTMLInputElement>(null);
13+
14+
return (
15+
<Styled.Wrapper onClick={() => inputRef.current?.focus()}>
16+
{leadingIcon}
17+
<Styled.Input ref={inputRef} {...rest} />
18+
{actionIcon}
19+
</Styled.Wrapper>
20+
);
21+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import { StoryObj, Meta } from '@storybook/react';
3+
import { action } from '@storybook/addon-actions';
4+
5+
import { Input } from '../Input';
6+
import { EyeIcon } from '../../../../icons';
7+
import { SearchIcon } from '../../../../icons/experimental';
8+
9+
const meta: Meta = {
10+
title: 'Experimental/Components/Input',
11+
component: Input,
12+
parameters: {
13+
layout: 'centered'
14+
},
15+
args: {
16+
placeholder: 'Placeholder'
17+
}
18+
};
19+
20+
export default meta;
21+
22+
type Story = StoryObj<typeof Input>;
23+
24+
export const Default: Story = {};
25+
26+
export const Disabled: Story = {
27+
args: {
28+
disabled: true
29+
}
30+
};
31+
32+
export const WithLeadingIcon: Story = {
33+
args: {
34+
type: 'search',
35+
leadingIcon: <SearchIcon />
36+
}
37+
};
38+
39+
export const WithActionIcon: Story = {
40+
args: {
41+
type: 'password',
42+
actionIcon: <EyeIcon onClick={action('Show password')} />
43+
}
44+
};
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
import React, { ReactElement } from 'react';
2-
import { SearchFieldProps } from 'react-aria-components';
2+
import { SearchFieldProps, SearchField } from 'react-aria-components';
33
import XCrossCircleIcon from '../../../icons/actions/XCrossCircleIcon';
4-
5-
import * as Styled from './Search.styled';
4+
import SearchIcon from '../../../icons/experimental/SearchIcon';
5+
import { Input } from '../Input/Input';
6+
import { Button } from '../Field/Button';
7+
import { getSemanticValue } from '../../../essentials/experimental';
68

79
interface SearchProps extends SearchFieldProps {
810
placeholder: string;
911
}
1012

1113
export const Search = ({ placeholder, ...rest }: SearchProps): ReactElement => (
12-
<Styled.SearchField aria-label={placeholder} {...rest}>
14+
<SearchField aria-label={placeholder} {...rest}>
1315
{({ state }) => (
14-
<>
15-
<Styled.Icon size={20} />
16-
<Styled.Input placeholder={placeholder} />
17-
{state.value !== '' && (
18-
<Styled.Button>
19-
<XCrossCircleIcon size={20} />
20-
</Styled.Button>
21-
)}
22-
</>
16+
<Input
17+
leadingIcon={<SearchIcon />}
18+
placeholder={placeholder}
19+
actionIcon={
20+
state.value !== '' && (
21+
<Button>
22+
<XCrossCircleIcon size={20} color={getSemanticValue('on-surface')} />
23+
</Button>
24+
)
25+
}
26+
/>
2327
)}
24-
</Styled.SearchField>
28+
</SearchField>
2529
);

src/components/experimental/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { Dialog } from './Dialog/Dialog';
1010
export { Divider } from './Divider/Divider';
1111
export { IconButton } from './IconButton/IconButton';
1212
export { InlineSpinner } from './InlineSpinner/InlineSpinner';
13+
export { Input } from './Input/Input';
1314
export { Label } from './Label/Label';
1415
export { ListBox, ListBoxItem } from './ListBox/ListBox';
1516
export { Modal } from './Modal/Modal';

0 commit comments

Comments
 (0)