Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/#32] Input 공통 컴포넌트 제작 #47

Merged
merged 21 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config: StorybookConfig = {
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
"storybook-addon-theme-provider",
],
framework: {
name: "@storybook/react-vite",
Expand Down
3 changes: 3 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { withThemeProvider } from "storybook-addon-theme-provider";
import type { Preview } from "@storybook/react";
import { Provider } from "./provider";

const preview: Preview = {
parameters: {
Expand All @@ -9,6 +11,7 @@ const preview: Preview = {
},
},
},
decorators: [withThemeProvider(Provider)],
};

export default preview;
18 changes: 18 additions & 0 deletions .storybook/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ThemeProvider } from "styled-components";
import theme from "../src/styles/theme";
import React, { ReactNode } from "react";
import GlobalStyle from "../src/styles/global";

interface ProviderProps {
children?: ReactNode;
theme?: unknown;
}

export const Provider = ({ children }: ProviderProps) => {
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
{children}
</ThemeProvider>
);
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"prettier": "^3.3.2",
"prettier-linter-helpers": "^1.0.0",
"storybook": "^8.1.11",
"storybook-addon-theme-provider": "^0.2.2",
"stylelint": "^16.6.1",
"stylelint-config-concentric-order": "^5.2.0",
"stylelint-config-prettier": "^9.0.5",
Expand Down
Empty file removed src/components/commons/.gitkeep
Empty file.
29 changes: 29 additions & 0 deletions src/components/commons/inputs/textAreas/TextArea.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import TextArea from "./TextArea";
import { useState } from "react";

const meta = {
title: "TextArea",
component: TextArea,
parameters: {
layout: "centered",
},
argTypes: {},
args: { value: "", onClick: fn(), placeholder: "플레이스 홀더", onChangeValue: fn() },
} satisfies Meta<typeof TextArea>;

export default meta;
type Story = StoryObj<typeof meta>;

const Template = (args) => {
const [value, setValue] = useState(args.value);

return <TextArea {...args} value={value} onChangeValue={setValue} />;
};

export const CapOn: Story = Template.bind({});
CapOn.args = { maxLength: 300 };

export const CapOff: Story = Template.bind({});
CapOff.args = {};
49 changes: 49 additions & 0 deletions src/components/commons/inputs/textAreas/TextArea.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Generators } from "@styles/generator";
import styled from "styled-components";

export const TextAreaWrapper = styled.section`
position: relative;
width: 32.7rem;
`;

export const TextAreaInput = styled.textarea`
${Generators.flexGenerator("row", "center", "center")}
width: 100%;
height: 12.9rem;
padding: 1.8rem;

color: ${({ theme }) => theme.colors.gray_0};

background: ${({ theme }) => theme.colors.gray_800};
outline: none;
border: 1px solid transparent;
border-radius: 0.6rem;

resize: none;

${({ theme }) => theme.fonts["body2-long"]};

&::placeholder {
color: ${({ theme }) => theme.colors.gray_600};
}

&:focus {
border: 1px solid ${({ theme }) => theme.colors.gray_0};
}

&::-webkit-scrollbar {
display: none;
}
`;

export const TextCap = styled.p`
${Generators.flexGenerator("row", "center", "end")}

width: 100%;
margin: 0;
margin-top: 0.6rem;

color: ${({ theme }) => theme.colors.gray_500};
text-align: right;
${({ theme }) => theme.fonts["body2-normal-medi"]};
`;
32 changes: 32 additions & 0 deletions src/components/commons/inputs/textAreas/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeEvent, TextareaHTMLAttributes } from "react";
import * as S from "./TextArea.styled";

export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
onChangeValue: (value: string) => void;
maxLength?: number;
placeholder: string;
}

const TextArea = ({ value, onChangeValue, maxLength, placeholder, ...rest }: TextAreaProps) => {
const handleOnInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
let inputValue = e.target.value;
if (maxLength && inputValue.length > maxLength) {
inputValue = inputValue.slice(0, maxLength);
}
onChangeValue(inputValue);
};
return (
<S.TextAreaWrapper>
<S.TextAreaInput
{...rest}
value={value}
onChange={handleOnInput}
maxLength={maxLength}
placeholder={placeholder}
/>
{maxLength && <S.TextCap>{`${(value as string).length}/${maxLength}`}</S.TextCap>}
</S.TextAreaWrapper>
);
};

export default TextArea;
44 changes: 44 additions & 0 deletions src/components/commons/inputs/textFields/TextField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import TextField from "./TextField";
import { numericFilter, phoneNumberFilter, priceFilter } from "../../../../utils/useInputFilter";
import { useState } from "react";

const meta = {
title: "TextField",
component: TextField,
parameters: {
layout: "centered",
},
argTypes: {},
args: { value: "", onClick: fn(), placeholder: "플레이스 홀더", onChangeValue: fn() },
} satisfies Meta<typeof TextField>;

export default meta;
type Story = StoryObj<typeof meta>;
const Template = (args) => {
const [value, setValue] = useState(args.value);

return <TextField {...args} value={value} onChangeValue={setValue} />;
};

export const DefaultCapOn: Story = Template.bind({});
DefaultCapOn.args = { maxLength: 30 };

export const DefaultCapOff: Story = Template.bind({});
DefaultCapOff.args = {};

export const Narrow: Story = Template.bind({});
Narrow.args = { narrow: true };

export const Time: Story = Template.bind({});
Time.args = { unit: "time", filter: numericFilter };

export const Ticket: Story = Template.bind({});
Ticket.args = { unit: "ticket", filter: numericFilter };

export const Amount: Story = Template.bind({});
Amount.args = { unit: "amount", filter: priceFilter };

export const Phone: Story = Template.bind({});
Phone.args = { filter: phoneNumberFilter };
63 changes: 63 additions & 0 deletions src/components/commons/inputs/textFields/TextField.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { IconTextfiedlDelete } from "@assets/svgs";
import { Generators } from "@styles/generator";
import styled from "styled-components";

export const TextFieldLayout = styled.section<{ narrow?: false | true }>`
position: relative;
width: ${({ narrow }) => (narrow ? "13.6rem" : "32.7rem")};
`;

export const TextFieldWrapper = styled.article`
${Generators.flexGenerator("row", "center", "center")}
`;

export const TextFieldInput = styled.input`
width: 100%;
height: 4.8rem;
padding: 0 1.6rem;

color: ${({ theme }) => theme.colors.gray_0};

background: ${({ theme }) => theme.colors.gray_800};
border: 1px solid transparent;
border-radius: 0.6rem;

${({ theme }) => theme.fonts["body2-normal-medi"]};

&::placeholder {
color: ${({ theme }) => theme.colors.gray_600};
}

&:focus {
border: 1px solid ${({ theme }) => theme.colors.gray_0};
}
`;

export const TextClear = styled(IconTextfiedlDelete)`
position: absolute;
top: 1.2rem;
right: 1.2rem;
width: 2.4rem;

cursor: pointer;
`;

export const TextUnit = styled.p`
position: absolute;
right: 1.6rem;

color: ${({ theme }) => theme.colors.gray_0};
${({ theme }) => theme.fonts["body2-normal-medi"]};
`;

export const TextCap = styled.p`
${Generators.flexGenerator("row", "center", "end")}

width: 100%;
margin: 0;
margin-top: 0.6rem;

color: ${({ theme }) => theme.colors.gray_500};
text-align: right;
${({ theme }) => theme.fonts["body2-normal-medi"]};
`;
60 changes: 60 additions & 0 deletions src/components/commons/inputs/textFields/TextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ChangeEvent, InputHTMLAttributes } from "react";
import * as S from "./TextField.styled";

export interface TextFieldProps extends InputHTMLAttributes<HTMLInputElement> {
onChangeValue: (value: string) => void;
maxLength?: number;
placeholder: string;
narrow?: false | true;
unit?: "time" | "ticket" | "amount"; // 단위 : "분", "매", "원"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 주석 최고 ㅎㅎ

filter?: (value: string) => string;
}

const TextField = ({
value,
onChangeValue,
maxLength,
placeholder,
narrow,
unit,
filter,
...rest
}: TextFieldProps) => {
const label = unit === "time" ? "분" : unit === "ticket" ? "매" : "원";

// 값 입력될 떄
const handleOnInput = (e: ChangeEvent<HTMLInputElement>) => {
let inputValue = e.target.value;
if (filter) {
inputValue = filter(inputValue);
}
if (maxLength && inputValue.length > maxLength) {
inputValue = inputValue.slice(0, maxLength);
}
onChangeValue(inputValue);
};

// 값 지울 때
const handleClearInput = () => {
onChangeValue("");
};

return (
<S.TextFieldLayout narrow={narrow}>
<S.TextFieldWrapper>
<S.TextFieldInput
{...rest}
value={value}
onChange={handleOnInput}
maxLength={maxLength}
placeholder={placeholder}
/>
{!narrow && !unit && value && <S.TextClear onClick={handleClearInput} />}
{unit && <S.TextUnit>{label}</S.TextUnit>}
</S.TextFieldWrapper>
{maxLength && <S.TextCap>{`${(value as string).length}/${maxLength}`}</S.TextCap>}
</S.TextFieldLayout>
);
};

export default TextField;
52 changes: 0 additions & 52 deletions src/components/ex-stories/Button.stories.ts

This file was deleted.

Loading
Loading