Skip to content

Commit

Permalink
Merge pull request #75 from sopt-makers/fix/ui
Browse files Browse the repository at this point in the history
[UI] feat: textarea 스크롤 추가, 엔터키로 submit
  • Loading branch information
sohee-K authored Apr 17, 2024
2 parents f7420a8 + bb61371 commit e677676
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-weeks-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sopt-makers/ui": patch
---

Textarea custom scroll, add keypress, fix padding
9 changes: 6 additions & 3 deletions apps/docs/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Test, TextField, TextArea, SearchField } from '@sopt-makers/ui';
import { Test } from '@sopt-makers/ui';
import './App.css';
import { useState, ChangeEvent } from 'react';
import TextField from '../../../packages/ui/Input/TextField';
import TextArea from '../../../packages/ui/Input/TextArea';
import SearchField from '../../../packages/ui/Input/SearchField';

function App() {
const [input, setInput] = useState('');
Expand Down Expand Up @@ -38,8 +41,8 @@ function App() {
return (
<>
<Test text="Test Component" size="big" color="blue" />
<TextField<string> labelText="Label" placeholder="Placeholder..." required descriptionText="description" validationFn={inputValidation} value={input} onChange={handleInputChange} />
<TextArea labelText="Label" placeholder="Placeholder..." required descriptionText="description" validationFn={textareaValidation} value={textarea} onChange={handleTextareaChange} onSubmit={handleTextareaSubmit} maxLength={300} />
<TextField<string> placeholder="Placeholder..." required labelText="Label" descriptionText="description" validationFn={inputValidation} value={input} onChange={handleInputChange} />
<TextArea placeholder="Placeholder..." required labelText="Label" descriptionText="description" validationFn={textareaValidation} value={textarea} onChange={handleTextareaChange} onSubmit={handleTextareaSubmit} maxLength={300} />
<SearchField placeholder="Placeholder..." value={search} onChange={handleSearchChange} onSubmit={handleSearchSubmit} onReset={handleSearchReset} />
</>
);
Expand Down
13 changes: 10 additions & 3 deletions packages/ui/Input/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ interface SearchFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, '
value: string;
onSubmit: () => void;
onReset: () => void;
disableEnterSubmit?: boolean;
}

function SearchField(props: SearchFieldProps) {
const { className, value, onSubmit, onReset, ...inputProps } = props;
const { className, value, onSubmit, onReset, disableEnterSubmit = false, ...inputProps } = props;

const [isFocused, setIsFocused] = useState(false);

const disabled = inputProps.disabled || inputProps.readOnly || value.length === 0;

const handleFocus = () => {
setIsFocused(true);
}
Expand All @@ -26,10 +29,14 @@ function SearchField(props: SearchFieldProps) {
setIsFocused(false);
}

const disabled = inputProps.disabled || inputProps.readOnly || value.length === 0;
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (!disableEnterSubmit && !disabled && event.key === 'Enter') {
onSubmit();
}
};

return <div className={className} style={{ position: 'relative' }}>
<input className={`${S.input} ${S.searchField}`} onBlur={handleBlur} onFocus={handleFocus} type="text" value={value} {...inputProps} />
<input {...inputProps} className={`${S.input} ${S.searchField}`} onBlur={handleBlur} onFocus={handleFocus} onKeyDown={handleKeyPress} type="text" value={value} />
{!disabled && isFocused ?
<button className={S.submitButton} type="reset">
<XCircleIcon />
Expand Down
34 changes: 27 additions & 7 deletions packages/ui/Input/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ChangeEvent, type TextareaHTMLAttributes } from 'react';
import { useState, type ChangeEvent, type TextareaHTMLAttributes } from 'react';
import * as S from './style.css';
import AlertCircleIcon from './icons/AlertCircleIcon';
import SendIcon from './icons/SendIcon';
Expand All @@ -10,39 +10,59 @@ interface TextAreaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>
errorMessage?: string;
value: string;
maxLength: number;
height?: string;
// isError -> validationFn 순서로 적용
isError?: boolean;
validationFn?: (input: string) => boolean;
onSubmit: () => void;
disableEnterSubmit?: boolean;
lineHeight?: number; // px
fixedHeight?: number; // px
}

function TextArea(props: TextAreaProps) {
const { className, labelText, descriptionText, errorMessage, value, maxLength, height, isError, validationFn, onSubmit, ...inputProps } = props;
const { className, labelText, descriptionText, errorMessage, value, maxLength, isError, validationFn, onSubmit, disableEnterSubmit = false, lineHeight = 26, fixedHeight, ...inputProps } = props;
const { onChange, ...restInputProps } = inputProps;

const [calcHeight, setCalcHeight] = useState(48);

const hasError = () => {
if (inputProps.disabled || inputProps.readOnly) return false;
if (isError !== undefined) return isError;
if (validationFn && !validationFn(value)) return true;
return false;
}

const disabled = inputProps.disabled || inputProps.readOnly || value.length === 0 || hasError();

const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const text = e.target.value;
const slicedText = text.slice(0, maxLength);
onChange && onChange({ ...e, target: { ...e.target, value: slicedText } });

if (!fixedHeight) {
const lines = (slicedText.match(/\n/g) || []).length;
const height = 48 + lineHeight * (lines > 4 ? 4 : lines);
setCalcHeight(height);
}
}

const disabled = inputProps.disabled || inputProps.readOnly || value.length === 0 || hasError();
const handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!disableEnterSubmit && event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
!disabled && onSubmit();
}
};

const buttonPosition = 48 + ((fixedHeight ?? calcHeight) - 48) / 2;

const required = inputProps.required ? <span className={S.required}>*</span> : null;
const description = descriptionText ? <p className={S.description}>{descriptionText}</p> : null;
const input = <textarea className={`${S.input} ${S.textarea} ${hasError() ? S.inputError : ''}`} onChange={handleInputChange} style={{ height }} value={value} {...restInputProps} />;
const input = <textarea {...restInputProps} className={`${S.input} ${S.textarea} ${hasError() ? S.inputError : ''}`} onChange={handleInputChange} onKeyDown={handleKeyPress} style={{ height: `${fixedHeight ?? calcHeight}px` }} value={value} />;

return <div className={className} style={{ position: 'relative' }}>
{labelText ? <label className={S.label}><span>{labelText}{required}</span>{description}{input}</label> : <>{description}{input}</>}
{labelText ? <label className={S.label}><span>{labelText}{required}</span>{description}{input}</label> : <div className={S.inputWrap}>{description}{input}</div>}

<button className={S.submitButton} disabled={disabled} onClick={onSubmit} style={{ transform: `translateY(-48px)` }} type="submit"><SendIcon disabled={disabled} /></button>
<button className={S.submitButton} disabled={disabled} onClick={onSubmit} style={{ transform: `translateY(-${buttonPosition}px)` }} type="submit"><SendIcon disabled={disabled} /></button>

<div className={S.inputBottom}>
{hasError() ? <div className={S.errorMessage}><AlertCircleIcon /><p>{errorMessage ?? 'error'}</p></div> : <div> </div>}
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/Input/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ function TextField<T extends string | number>(props: TextFieldProps<T>) {

const required = inputProps.required ? <span className={S.required}>*</span> : null;
const description = descriptionText ? <p className={S.description}>{descriptionText}</p> : null;
const input = <input className={`${S.input} ${hasError() ? S.inputError : ''}`} value={value} {...inputProps} />;
const input = <input {...inputProps} className={`${S.input} ${hasError() ? S.inputError : ''}`} value={value} />;

return <div className={className}>
{labelText ? <label className={S.label}><span>{labelText}{required}</span>{description}{input}</label> : <>{description}{input}</>}
{labelText ? <label className={S.label}><span>{labelText}{required}</span>{description}{input}</label> : <div className={S.inputWrap}>{description}{input}</div>}
{hasError() ? <div className={S.inputBottom}><div className={S.errorMessage}><AlertCircleIcon /><p>{errorMessage ?? 'error'}</p></div></div> : null}
</div>
}
Expand Down
49 changes: 30 additions & 19 deletions packages/ui/Input/style.css.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { style } from "@vanilla-extract/css";
import { globalStyle, style } from "@vanilla-extract/css";
import theme from "../theme.css";

export const label = style({
Expand All @@ -11,13 +11,12 @@ export const label = style({

export const input = style({
...theme.fontsObject.BODY_2_16_M,
marginTop: "8px",
background: theme.colors.gray800,
border: "1px solid transparent",
borderRadius: "10px",
width: "100%",
height: "48px",
padding: "11px 16px",
padding: "10px 16px",
color: theme.colors.white,
boxSizing: "border-box",

Expand All @@ -37,29 +36,33 @@ export const input = style({
});

export const textarea = style({
overflow: "hidden",
resize: "none",
paddingRight: "48px",
paddingRight: 0,
display: 'block',

// "::-webkit-scrollbar": {
// width: "16px",
// },
// "::-webkit-scrollbar-thumb": {
// backgroundColor: theme.colors.gray500,
// backgroundClip: "padding-box",
// border: "4px solid transparent",
// borderRadius: "16px",
// },
// "::-webkit-scrollbar-track": {
// backgroundColor: "transparent",
// },
"::-webkit-scrollbar": {
width: "48px",
},
"::-webkit-scrollbar-thumb": {
backgroundColor: theme.colors.gray500,
backgroundClip: "padding-box",
border: "4px solid transparent",
boxShadow: `inset -36px 0 0 ${theme.colors.gray800}`,
borderRadius: "6px",
},
"::-webkit-scrollbar-track": {
backgroundColor: "transparent",
},
});

export const searchField = style({
marginTop: "0",
paddingRight: "48px",
});

export const inputWrap = style({
textAlign: 'left',
});

export const inputError = style({
border: `1px solid ${theme.colors.error}`,
});
Expand All @@ -79,7 +82,7 @@ export const required = style({
export const description = style({
...theme.fontsObject.LABEL_4_12_SB,
color: theme.colors.gray300,
marginTop: "8px",
marginBottom: '8px',
});

export const errorMessage = style({
Expand Down Expand Up @@ -113,3 +116,11 @@ export const submitButton = style({
cursor: "not-allowed",
},
});

globalStyle(`${inputWrap} > ${description}`, {
marginTop: 0
});

globalStyle(`${label} > span`, {
marginBottom: "8px"
});

0 comments on commit e677676

Please sign in to comment.