Skip to content

Commit a198fcd

Browse files
authored
🪟 🎨 🧹 Migrate Input and TextArea to SCSS (#16378)
* Migrate TextArea component to SCSS and add Storybook * Move Input styles to SCSS, add Storybook * Fix Input stylelint issues * Fix hover selector on Input container to avoid hovering on focus * Fix Input focus test by using style file * Add missing & to Textarea style * Fix styleint inssue in Input * Move input testid before props
1 parent 0172360 commit a198fcd

File tree

7 files changed

+215
-119
lines changed

7 files changed

+215
-119
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
@use "../../../scss/colors";
2+
3+
.container {
4+
width: 100%;
5+
position: relative;
6+
background-color: colors.$grey-50;
7+
border: 1px solid colors.$grey-50;
8+
border-radius: 4px;
9+
10+
&.light {
11+
background-color: colors.$white;
12+
}
13+
14+
&.error {
15+
background-color: colors.$grey-100;
16+
border-color: colors.$red;
17+
}
18+
19+
&:not(.disabled, .focused):hover {
20+
background-color: colors.$grey-100;
21+
border-color: colors.$grey-100;
22+
23+
&.light {
24+
background-color: colors.$white;
25+
}
26+
27+
&.error {
28+
border-color: colors.$red;
29+
}
30+
}
31+
32+
&.focused {
33+
background-color: colors.$primaryColor12;
34+
border-color: colors.$blue;
35+
36+
&.light {
37+
background-color: colors.$white;
38+
}
39+
}
40+
}
41+
42+
.input {
43+
outline: none;
44+
width: 100%;
45+
padding: 7px 8px;
46+
font-size: 14px;
47+
line-height: 20px;
48+
font-weight: normal;
49+
border: none;
50+
background: none;
51+
color: colors.$dark-blue;
52+
caret-color: colors.$blue;
53+
54+
&:not(.disabled).password {
55+
width: calc(100% - 22px);
56+
}
57+
58+
&::placeholder {
59+
color: colors.$grey-300;
60+
}
61+
62+
&.disabled {
63+
pointer-events: none;
64+
color: colors.$grey-400;
65+
}
66+
}
67+
68+
button.visibilityButton {
69+
position: absolute;
70+
right: 0;
71+
top: 0;
72+
display: flex;
73+
height: 100%;
74+
width: 30px;
75+
align-items: center;
76+
justify-content: center;
77+
border: none;
78+
}

airbyte-webapp/src/components/base/Input/Input.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { act } from "react-dom/test-utils";
33
import { render } from "test-utils/testutils";
44

55
import { Input } from "./Input";
6+
// eslint-disable-next-line css-modules/no-unused-class
7+
import styles from "./Input.module.scss";
68

79
describe("<Input />", () => {
810
test("renders text input", async () => {
@@ -117,7 +119,7 @@ describe("<Input />", () => {
117119
fireEvent.focus(inputEl);
118120
fireEvent.focus(inputEl);
119121

120-
expect(getByTestId("input-container")).toHaveClass("input-container--focused");
122+
expect(getByTestId("input-container")).toHaveClass(styles.focused);
121123
});
122124

123125
test("does not have focused class after blur", async () => {
@@ -128,7 +130,7 @@ describe("<Input />", () => {
128130
fireEvent.blur(inputEl);
129131
fireEvent.blur(inputEl);
130132

131-
expect(getByTestId("input-container")).not.toHaveClass("input-container--focused");
133+
expect(getByTestId("input-container")).not.toHaveClass(styles.focused);
132134
});
133135

134136
test("calls onFocus if passed as prop", async () => {

airbyte-webapp/src/components/base/Input/Input.tsx

Lines changed: 27 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,85 +4,16 @@ import classNames from "classnames";
44
import React, { useCallback, useRef, useState } from "react";
55
import { useIntl } from "react-intl";
66
import { useToggle } from "react-use";
7-
import styled from "styled-components";
8-
import { Theme } from "theme";
97

108
import Button from "../Button";
11-
12-
type IStyleProps = InputProps & { theme: Theme };
13-
14-
const getBackgroundColor = (props: IStyleProps) => {
15-
if (props.error) {
16-
return props.theme.greyColor10;
17-
} else if (props.light) {
18-
return props.theme.whiteColor;
19-
}
20-
21-
return props.theme.greyColor0;
22-
};
9+
import styles from "./Input.module.scss";
2310

2411
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
2512
error?: boolean;
2613
light?: boolean;
2714
}
2815

29-
const InputContainer = styled.div<InputProps>`
30-
width: 100%;
31-
position: relative;
32-
background: ${(props) => getBackgroundColor(props)};
33-
border: 1px solid ${(props) => (props.error ? props.theme.dangerColor : props.theme.greyColor0)};
34-
border-radius: 4px;
35-
36-
${({ disabled, theme, light, error }) =>
37-
!disabled &&
38-
`
39-
&:hover {
40-
background: ${light ? theme.whiteColor : theme.greyColor20};
41-
border-color: ${error ? theme.dangerColor : theme.greyColor20};
42-
}
43-
`}
44-
45-
&.input-container--focused {
46-
background: ${({ theme, light }) => (light ? theme.whiteColor : theme.primaryColor12)};
47-
border-color: ${({ theme }) => theme.primaryColor};
48-
}
49-
`;
50-
51-
const InputComponent = styled.input<InputProps & { isPassword?: boolean }>`
52-
outline: none;
53-
width: ${({ isPassword, disabled }) => (isPassword && !disabled ? "calc(100% - 22px)" : "100%")};
54-
padding: 7px 8px 7px 8px;
55-
font-size: 14px;
56-
line-height: 20px;
57-
font-weight: normal;
58-
border: none;
59-
background: none;
60-
color: ${({ theme }) => theme.textColor};
61-
caret-color: ${({ theme }) => theme.primaryColor};
62-
63-
&::placeholder {
64-
color: ${({ theme }) => theme.greyColor40};
65-
}
66-
67-
&:disabled {
68-
pointer-events: none;
69-
color: ${({ theme }) => theme.greyColor55};
70-
}
71-
`;
72-
73-
const VisibilityButton = styled(Button)`
74-
position: absolute;
75-
right: 0px;
76-
top: 0;
77-
display: flex;
78-
height: 100%;
79-
width: 30px;
80-
align-items: center;
81-
justify-content: center;
82-
border: none;
83-
`;
84-
85-
const Input: React.FC<InputProps> = ({ ...props }) => {
16+
export const Input: React.FC<InputProps> = ({ light, error, ...props }) => {
8617
const { formatMessage } = useIntl();
8718

8819
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -137,15 +68,34 @@ const Input: React.FC<InputProps> = ({ ...props }) => {
13768
};
13869

13970
return (
140-
<InputContainer
141-
className={classNames("input-container", { "input-container--focused": focused })}
71+
<div
72+
className={classNames(styles.container, {
73+
[styles.disabled]: props.disabled,
74+
[styles.focused]: focused,
75+
[styles.light]: light,
76+
[styles.error]: error,
77+
})}
14278
data-testid="input-container"
14379
onFocus={onContainerFocus}
14480
onBlur={onContainerBlur}
14581
>
146-
<InputComponent data-testid="input" {...props} ref={inputRef} type={type} isPassword={isPassword} />
82+
<input
83+
data-testid="input"
84+
{...props}
85+
ref={inputRef}
86+
type={type}
87+
className={classNames(
88+
styles.input,
89+
{
90+
[styles.disabled]: props.disabled,
91+
[styles.password]: isPassword,
92+
},
93+
props.className
94+
)}
95+
/>
14796
{isVisibilityButtonVisible ? (
148-
<VisibilityButton
97+
<Button
98+
className={styles.visibilityButton}
14999
ref={buttonRef}
150100
iconOnly
151101
onClick={() => {
@@ -159,11 +109,10 @@ const Input: React.FC<InputProps> = ({ ...props }) => {
159109
data-testid="toggle-password-visibility-button"
160110
>
161111
<FontAwesomeIcon icon={isContentVisible ? faEyeSlash : faEye} fixedWidth />
162-
</VisibilityButton>
112+
</Button>
163113
) : null}
164-
</InputContainer>
114+
</div>
165115
);
166116
};
167117

168118
export default Input;
169-
export { Input };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ComponentStory, ComponentMeta } from "@storybook/react";
2+
3+
import Input from "./Input";
4+
5+
export default {
6+
title: "Ui/Input",
7+
component: Input,
8+
argTypes: {
9+
disabled: { control: "boolean" },
10+
type: { control: { type: "select", options: ["text", "number", "password"] } },
11+
},
12+
} as ComponentMeta<typeof Input>;
13+
14+
const Template: ComponentStory<typeof Input> = (args) => <Input {...args} />;
15+
16+
export const Primary = Template.bind({});
17+
Primary.args = {
18+
placeholder: "Enter text here...",
19+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@use "../../../scss/colors";
2+
@use "../../../scss/variables";
3+
4+
.textarea {
5+
outline: none;
6+
resize: none;
7+
width: 100%;
8+
padding: 7px 8px;
9+
border-radius: 4px;
10+
font-size: 14px;
11+
line-height: 20px;
12+
font-weight: normal;
13+
border: 1px solid colors.$grey-50;
14+
background-color: colors.$grey-50;
15+
color: colors.$dark-blue;
16+
caret-color: colors.$blue;
17+
18+
&.error {
19+
border-color: colors.$red;
20+
}
21+
22+
&::placeholder {
23+
color: colors.$grey-300;
24+
}
25+
26+
&:hover {
27+
background-color: colors.$grey-100;
28+
border-color: colors.$grey-100;
29+
30+
&.light {
31+
background-color: colors.$white;
32+
}
33+
34+
&.error {
35+
border-color: colors.$red;
36+
}
37+
}
38+
39+
&:focus {
40+
background-color: colors.$primaryColor12;
41+
border-color: colors.$blue;
42+
43+
&.light {
44+
background-color: colors.$white;
45+
}
46+
}
47+
48+
&:disabled {
49+
pointer-events: none;
50+
color: colors.$grey-400;
51+
}
52+
}
Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,25 @@
1+
import classNames from "classnames";
12
import React from "react";
2-
import styled from "styled-components";
33

4-
type TextAreaProps = {
4+
import styles from "./TextArea.module.scss";
5+
6+
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
57
error?: boolean;
68
light?: boolean;
7-
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
8-
9-
const TextArea = styled.textarea<TextAreaProps>`
10-
outline: none;
11-
resize: none;
12-
width: 100%;
13-
padding: 7px 8px;
14-
border-radius: 4px;
15-
font-size: 14px;
16-
line-height: 20px;
17-
font-weight: normal;
18-
border: 1px solid ${(props) => (props.error ? props.theme.dangerColor : props.theme.greyColor0)};
19-
background: ${({ theme }) => theme.greyColor0};
20-
color: ${({ theme }) => theme.textColor};
21-
caret-color: ${({ theme }) => theme.primaryColor};
22-
23-
&::placeholder {
24-
color: ${({ theme }) => theme.greyColor40};
25-
}
26-
27-
&:hover {
28-
background: ${({ theme, light }) => (light ? theme.whiteColor : theme.greyColor20)};
29-
border-color: ${(props) => (props.error ? props.theme.dangerColor : props.theme.greyColor20)};
30-
}
31-
32-
&:focus {
33-
background: ${({ theme, light }) => (light ? theme.whiteColor : theme.primaryColor12)};
34-
border-color: ${({ theme }) => theme.primaryColor};
35-
}
36-
37-
&:disabled {
38-
pointer-events: none;
39-
color: ${({ theme }) => theme.greyColor55};
40-
}
41-
`;
9+
}
4210

43-
export { TextArea };
44-
export type { TextAreaProps };
11+
export const TextArea: React.FC<TextAreaProps> = ({ error, light, children, className, ...textAreaProps }) => (
12+
<textarea
13+
{...textAreaProps}
14+
className={classNames(
15+
styles.textarea,
16+
{
17+
[styles.error]: error,
18+
[styles.light]: light,
19+
},
20+
className
21+
)}
22+
>
23+
{children}
24+
</textarea>
25+
);

0 commit comments

Comments
 (0)