Skip to content

Commit ec45762

Browse files
authored
Fix Inputs interactive area (#600)
* fix(InputWrapper): make horizontal paddings interactive * refactor: cleanup unused code * feat(TextField): render extra content before or after control * refactor(SearchField): rebuild on top of TextField * fix(PasswordField): use InputEndContent for eye button * fix: don't render InputEndContent instead of hiding it * docs: autogenerate API docs for Input * fix(DatePicker): use InputStartContent
1 parent ea91fa2 commit ec45762

File tree

9 files changed

+212
-247
lines changed

9 files changed

+212
-247
lines changed

src/components/AutoComplete/AutoComplete.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe("AutoComplete", () => {
213213
expect(queryByText("Content3")).not.toBeNull();
214214
expect(queryByText("Content1 long text content")).toBeNull();
215215
expect(queryByText("Group label")).not.toBeVisible();
216-
fireEvent.click(getByTestId("search-close"));
216+
fireEvent.click(getByTestId("textfield-clear"));
217217
expect(queryByText("Group label")).toBeVisible();
218218
expect(queryByText("Content0")).not.toBeNull();
219219
expect(queryByText("Content1 long text content")).not.toBeNull();

src/components/DatePicker/Common.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import styled from "styled-components";
2-
import { InputElement, InputWrapper } from "../Input/InputWrapper";
2+
import { InputElement, InputStartContent, InputWrapper } from "../Input/InputWrapper";
33
import { ReactNode, useCallback, useId } from "react";
44
import { Icon } from "../Icon/Icon";
55
import { Container } from "../Container/Container";
@@ -53,8 +53,11 @@ export const DatePickerInput = ({
5353
disabled={disabled}
5454
id={id ?? defaultId}
5555
>
56-
<Icon name="calendar" />
56+
<InputStartContent>
57+
<Icon name="calendar" />
58+
</InputStartContent>
5759
<InputElement
60+
$hasStartContent
5861
data-testid="datepicker-input"
5962
placeholder={placeholder}
6063
readOnly
@@ -120,8 +123,11 @@ export const DateRangePickerInput = ({
120123
disabled={disabled}
121124
id={id ?? defaultId}
122125
>
123-
<Icon name="calendar" />
126+
<InputStartContent>
127+
<Icon name="calendar" />
128+
</InputStartContent>
124129
<InputElement
130+
$hasStartContent
125131
as="div"
126132
data-testid="daterangepicker-input"
127133
>

src/components/Input/InputWrapper.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const Wrapper = styled.div<{
5959
overflow: auto;
6060
`
6161
}
62-
padding: 0 ${theme.click.field.space.x};
6362
${
6463
$error
6564
? `
@@ -183,15 +182,20 @@ export const InputWrapper = ({
183182
);
184183
};
185184

186-
export const InputElement = styled.input`
185+
export const InputElement = styled.input<{
186+
$hasStartContent?: boolean;
187+
$hasEndContent?: boolean;
188+
}>`
187189
background: transparent;
188190
border: none;
189191
outline: none;
190192
width: 100%;
191193
color: inherit;
192194
font: inherit;
193-
${({ theme }) => `
195+
${({ theme, $hasStartContent, $hasEndContent }) => `
194196
padding: ${theme.click.field.space.y} 0;
197+
padding-left: ${$hasStartContent ? "0" : theme.click.field.space.x};
198+
padding-right: ${$hasEndContent ? "0" : theme.click.field.space.x};
195199
&::placeholder {
196200
color: ${theme.click.field.color.placeholder.default};
197201
}
@@ -230,21 +234,15 @@ export const TextAreaElement = styled.textarea`
230234
font: inherit;
231235
resize: none;
232236
${({ theme }) => `
233-
padding: ${theme.click.field.space.y} 0;
237+
padding: ${theme.click.field.space.y} ${theme.click.field.space.x};
234238
align-self: stretch;
235239
&::placeholder {
236240
color: ${theme.click.field.color.placeholder.default};
237241
}
238242
`}
239243
`;
240244

241-
export const TextAreaWrapper = styled(InputWrapper)`
242-
resize: vertical;
243-
overflow: auto;
244-
color: red;
245-
`;
246-
247-
export const IconButton = styled.button<{ $show?: boolean }>`
245+
export const IconButton = styled.button`
248246
background: transparent;
249247
color: inherit;
250248
border: none;
@@ -253,9 +251,8 @@ export const IconButton = styled.button<{ $show?: boolean }>`
253251
&:not(:disabled) {
254252
cursor: pointer;
255253
}
256-
${({ theme, $show }) => `
254+
${({ theme }) => `
257255
padding: ${theme.click.field.space.y} 0;
258-
visibility: ${$show ? "visible" : "hidden"};
259256
`}
260257
`;
261258

@@ -269,3 +266,24 @@ export const IconWrapper = styled.svg`
269266
}
270267
`}
271268
`;
269+
270+
export const InputStartContent = styled.div`
271+
${({ theme }) => `
272+
padding-left: ${theme.click.field.space.x};
273+
cursor: text;
274+
gap: ${theme.click.field.space.gap};
275+
display: flex;
276+
align-self: stretch;
277+
align-items: center;
278+
`}
279+
`;
280+
281+
export const InputEndContent = styled.div`
282+
${({ theme }) => `
283+
padding-right: ${theme.click.field.space.x};
284+
gap: ${theme.click.field.space.gap};
285+
display: flex;
286+
align-self: stretch;
287+
align-items: center;
288+
`}
289+
`;

src/components/Input/PasswordField.stories.tsx

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,33 @@
11
import { useEffect, useState } from "react";
2-
import { PasswordField as PasswordFieldInput, PasswordFieldProps } from "./PasswordField";
3-
import { Container } from "../Container/Container";
42

5-
const PasswordField = ({
6-
value: valueProp,
7-
...props
8-
}: Omit<PasswordFieldProps, "onChange">) => {
9-
const [value, setValue] = useState(valueProp);
10-
useEffect(() => {
11-
setValue(valueProp);
12-
}, [valueProp]);
3+
import { Meta, StoryObj } from "@storybook/react";
134

14-
return (
15-
<Container maxWidth="75%">
16-
<PasswordFieldInput
17-
value={value}
18-
onChange={(inputValue: string) => {
19-
setValue(inputValue);
20-
}}
21-
{...props}
22-
/>
23-
</Container>
24-
);
25-
};
5+
import { PasswordField } from "./PasswordField";
266

27-
export default {
7+
const meta: Meta<typeof PasswordField> = {
288
component: PasswordField,
299
title: "Forms/Input/PasswordField",
3010
tags: ["form-field", "input", "autodocs"],
31-
argTypes: {
32-
value: { control: "text" },
33-
label: { control: "text" },
34-
error: { control: "text" },
35-
disabled: { control: "boolean" },
36-
placeholder: { control: "text" },
37-
readOnly: { control: "boolean" },
38-
orientation: { control: "inline-radio", options: ["horizontal", "vertical"] },
39-
dir: { control: "inline-radio", options: ["start", "end"] },
11+
render: ({ value: valueProp, ...props }) => {
12+
const [value, setValue] = useState(valueProp);
13+
14+
useEffect(() => {
15+
setValue(valueProp);
16+
}, [valueProp]);
17+
18+
return (
19+
<PasswordField
20+
{...props}
21+
value={value}
22+
onChange={setValue}
23+
/>
24+
);
4025
},
4126
};
4227

43-
export const Playground = {
28+
export default meta;
29+
30+
export const Playground: StoryObj<typeof PasswordField> = {
4431
args: {
4532
label: "Label",
4633
disabled: false,

src/components/Input/PasswordField.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { ChangeEvent, InputHTMLAttributes, forwardRef, useId, useState } from "react";
22
import { Icon } from "@/components";
3-
import { IconButton, InputElement, InputWrapper, WrapperProps } from "./InputWrapper";
3+
import {
4+
IconButton,
5+
InputElement,
6+
InputEndContent,
7+
InputWrapper,
8+
WrapperProps,
9+
} from "./InputWrapper";
410
export interface PasswordFieldProps
511
extends Omit<WrapperProps, "id" | "children">,
612
Omit<
@@ -39,6 +45,8 @@ export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
3945
onChangeProp(e.target.value, e);
4046
};
4147

48+
const hasEndContent = value.length > 0;
49+
4250
return (
4351
<InputWrapper
4452
disabled={disabled}
@@ -49,6 +57,8 @@ export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
4957
dir={dir}
5058
>
5159
<InputElement
60+
$hasStartContent={false}
61+
$hasEndContent={hasEndContent}
5262
ref={ref}
5363
type={viewPassword ? "text" : "password"}
5464
id={id ?? defaultId}
@@ -57,16 +67,19 @@ export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
5767
onChange={onChange}
5868
{...props}
5969
/>
60-
<IconButton
61-
disabled={disabled}
62-
onClick={togglePasswordViewer}
63-
$show={value.length > 0}
64-
>
65-
<Icon
66-
name={viewPassword ? "eye-closed" : "eye"}
67-
size="md"
68-
/>
69-
</IconButton>
70+
{hasEndContent && (
71+
<InputEndContent>
72+
<IconButton
73+
disabled={disabled}
74+
onClick={togglePasswordViewer}
75+
>
76+
<Icon
77+
name={viewPassword ? "eye-closed" : "eye"}
78+
size="md"
79+
/>
80+
</IconButton>
81+
</InputEndContent>
82+
)}
7083
</InputWrapper>
7184
);
7285
}
Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,39 @@
11
import { useEffect, useState } from "react";
2-
import { SearchField as SearchFieldInput, SearchFieldProps } from "./SearchField";
2+
3+
import { Meta, StoryObj } from "@storybook/react";
4+
35
import { Container } from "../Container/Container";
46

5-
const SearchField = ({
6-
value: valueProp,
7-
...props
8-
}: Omit<SearchFieldProps, "onChange">) => {
9-
const [value, setValue] = useState(valueProp);
10-
useEffect(() => {
11-
setValue(valueProp);
12-
}, [valueProp]);
13-
14-
return (
15-
<Container maxWidth="350px">
16-
<SearchFieldInput
17-
value={value}
18-
onChange={(inputValue: string) => {
19-
setValue(inputValue);
20-
}}
21-
{...props}
22-
/>
23-
</Container>
24-
);
25-
};
7+
import { SearchField } from "./SearchField";
268

27-
export default {
9+
const meta: Meta<typeof SearchField> = {
2810
component: SearchField,
2911
title: "Forms/Input/SearchField",
3012
tags: ["form-field", "input", "autodocs"],
31-
argTypes: {
32-
value: { control: "text" },
33-
clear: { control: "boolean" },
34-
label: { control: "text" },
35-
error: { control: "text" },
36-
disabled: { control: "boolean" },
37-
placeholder: { control: "text" },
38-
readOnly: { control: "boolean" },
39-
orientation: { control: "inline-radio", options: ["horizontal", "vertical"] },
40-
dir: { control: "inline-radio", options: ["start", "end"] },
41-
isFilter: { control: "boolean" },
13+
render: ({ value: valueProp, ...props }) => {
14+
const [value, setValue] = useState(valueProp);
15+
16+
useEffect(() => {
17+
setValue(valueProp);
18+
}, [valueProp]);
19+
20+
return (
21+
<Container maxWidth="350px">
22+
<SearchField
23+
{...props}
24+
value={value}
25+
onChange={setValue}
26+
/>
27+
</Container>
28+
);
4229
},
4330
};
4431

45-
export const Playground = {
32+
export default meta;
33+
34+
export const Playground: StoryObj<typeof SearchField> = {
4635
args: {
4736
label: "Label",
48-
disabled: false,
4937
placeholder: "Placeholder",
50-
clear: false,
51-
readOnly: false,
52-
isFilter: false,
53-
value: "",
54-
error: "",
5538
},
5639
};

0 commit comments

Comments
 (0)