Skip to content

Commit

Permalink
[Feature/BAR-6] WriteInput 작성 (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
wonjin-dev authored Jan 10, 2024
1 parent 3f96bbc commit 1d695c7
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 24 deletions.
16 changes: 14 additions & 2 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
const HomePage = () => {
return <>Hello World</>;
import type { NextPage } from 'next';

import WriteInput from '@/src/components/Input/WriteInput';
import { useInput } from '@/src/hooks/useInput';

const HomePage: NextPage = () => {
const testInputProps = useInput({
id: 'test',
defaultValue: '',
});

return (
<WriteInput inputProps={testInputProps} placeholder="메모를 끄적여보세요" />
);
};

export default HomePage;
1 change: 0 additions & 1 deletion plopfile.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable import/no-anonymous-default-export */
export default function (plop) {
const getComponentName = {
type: 'input',
Expand Down
4 changes: 4 additions & 0 deletions src/assets/icons/submit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions src/components/Input/WriteInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { ChangeEvent, HTMLAttributes, KeyboardEvent } from 'react';
import { useMemo, useRef, useState } from 'react';
import { assignInlineVars } from '@vanilla-extract/dynamic';

import { MAIN_INPUT_MAX_LENGTH } from '@/src/constants/config';
import type { UseInputReturn } from '@/src/hooks/useInput';
import { COLORS } from '@/src/styles/tokens';

import Icon from '../../SvgIcon';
import * as style from './style.css';

interface WriteInputProps extends HTMLAttributes<HTMLTextAreaElement> {
inputProps: UseInputReturn;
placeholder?: string;
maxLength?: number;
}

const WriteInput = ({
inputProps,
placeholder,
maxLength = MAIN_INPUT_MAX_LENGTH,
}: WriteInputProps) => {
const { id, value } = inputProps;
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const [textareaHeight, setTextareaHeight] = useState({
row: 1,
lineBreak: {},
});

const handleResize = (e: ChangeEvent<HTMLTextAreaElement>) => {
const { scrollHeight, clientHeight, value } = e.target;

if (value.length === 0) {
setTextareaHeight((prev) => ({
row: 1,
lineBreak: { ...prev.lineBreak, [e.target.value.length]: false },
}));
}

if (scrollHeight > clientHeight) {
setTextareaHeight((prev) => ({
row: prev.row + 1,
lineBreak: { ...prev.lineBreak, [value.length - 1]: true },
}));
}

if (textareaHeight.lineBreak[value.length]) {
setTextareaHeight((prev) => ({
row: prev.row - 1,
lineBreak: { ...prev.lineBreak, [value.length]: false },
}));
}
};

const handleKeydownEnter = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.code === 'Enter') {
setTextareaHeight((prev) => ({
row: prev.row + 1,
lineBreak: { ...prev.lineBreak, [value.length]: true },
}));
}
};

const isValid = useMemo(() => value.length > 0, [value.length]);

return (
<div className={style.conatiner}>
<div
className={style.contentWrapper}
style={assignInlineVars({
[style.inputHeight]: `${textareaHeight.row * 27}px`,
})}
>
<label htmlFor={id} className={style.label}>
<textarea
{...inputProps}
ref={inputRef}
autoComplete="off"
rows={textareaHeight.row}
className={style.input}
placeholder={placeholder}
maxLength={maxLength}
onInput={handleResize}
onKeyDown={handleKeydownEnter}
/>
</label>

<div
className={style.submitWrapper({
multirow: textareaHeight.row > 1,
})}
>
<div className={style.submit}>
{value.length > 0 && (
<span className={style.textCount}>
<span className={style.currentTextCount}>{value.length}</span>
&nbsp;/&nbsp;500자
</span>
)}
<button disabled={!isValid}>
<Icon
icon="submit"
width={48}
height={48}
color={isValid ? COLORS['Blue/Default'] : undefined}
/>
</button>
</div>
</div>
</div>
</div>
);
};

export default WriteInput;
90 changes: 90 additions & 0 deletions src/components/Input/WriteInput/style.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createVar, style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import { COLORS } from '@/src/styles/tokens';

export const conatiner = style({
display: 'flex',
justifyContent: 'space-between',
borderRadius: '16px',
width: '100%',
padding: '22px 12px 22px 24px',
border: `2px solid ${COLORS['Blue/Gradient']}`,
});

export const inputHeight = createVar();
export const contentWrapper = style({
display: 'flex',
alignItems: 'center',
width: '100%',
height: inputHeight,
minHeight: '27px',
maxHeight: '260px',
});

export const label = style({
width: '100%',
});

export const input = style({
padding: '0',
width: '100%',
maxHeight: '216px',
resize: 'none',
color: COLORS['Grey/900'],
fontSize: '17px',
lineHeight: '27px',
overflowWrap: 'break-word',
'::placeholder': {
color: COLORS['Grey/250'],
},
});

export const submitWrapper = recipe({
base: {
display: 'flex',
alignItems: 'flex-end',
paddingLeft: '20px',
},
variants: {
multirow: {
true: {
height: '100%',
},
},
},
});

export const submit = style({
display: 'flex',
alignItems: 'center',
gap: '16px',
height: '48px',
});

export const textCount = style({
color: COLORS['Grey/400'],
fontSize: '14px',
fontWeight: '400',
whiteSpace: 'nowrap',
});

export const currentTextCount = style({
color: COLORS['Blue/Default'],
fontSize: '14px',
fontWeight: '700',
});

export const alert = style({
display: 'flex',
alignItems: 'center',
marginTop: '12px',
});

export const alertMsg = style({
marginLeft: '6px',
color: COLORS['Grey/600'],
fontSize: '13px',
fontWeight: '600',
lineHeight: '17px',
});
19 changes: 6 additions & 13 deletions src/components/SvgIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { Icon as icon } from '../constants/icon';
import { iconFactory, type Icons } from '../constants/icon';

interface IconProps {
icon: keyof typeof icon;
fill?: string;
stroke?: string;
icon: Icons;
color?: string;
width?: number;
height?: number;
}

const Icon = ({
icon: iconKey,
fill,
stroke,
width = 24,
height = 24,
}: IconProps) => {
const SvgIcon = icon[iconKey];
const Icon = ({ icon, color, width = 24, height = 24 }: IconProps) => {
const SvgIcon = iconFactory[icon];

return <SvgIcon fill={fill} stroke={stroke} width={width} height={height} />;
return <SvgIcon color={color} width={width} height={height} />;
};

export default Icon;
4 changes: 4 additions & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* "끄적끄적" 최대 입력 글자 수
*/
export const MAIN_INPUT_MAX_LENGTH = 500;
12 changes: 5 additions & 7 deletions src/constants/icon.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { FC, SVGProps } from 'react';

import Profle from '@/src/assets/icons/profile.svg';
import Submit from '@/src/assets/icons/submit.svg';

export type IconFactory = {
[key: string]: FC<SVGProps<SVGSVGElement>>;
};

export const Icon: IconFactory = {
export const iconFactory = {
profile: Profle,
submit: Submit,
};

export type Icons = keyof typeof iconFactory;
19 changes: 19 additions & 0 deletions src/hooks/useInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ChangeEvent } from 'react';
import { useState } from 'react';

interface UseInputArgs {
id: string;
defaultValue?: string;
}

export const useInput = ({ id, defaultValue = '' }: UseInputArgs) => {
const [value, setValue] = useState(defaultValue);

const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setValue(e.currentTarget.value);
};

return { id, value, onChange };
};

export type UseInputReturn = ReturnType<typeof useInput>;
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"strict": true,
"strict": false,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
Expand Down

0 comments on commit 1d695c7

Please sign in to comment.