-
Notifications
You must be signed in to change notification settings - Fork 5
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
[FE] 사용성 개선 - 지출 내역 입력 아이템 구현 #357
Changes from 4 commits
e4d8e3e
f326aaa
0cab0f8
ca61266
f5a0555
70086d6
3fa3996
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import {css} from '@emotion/react'; | ||
|
||
import {TextSize} from '@components/Text/Text.type'; | ||
|
||
import {Theme} from '@theme/theme.type'; | ||
|
||
import TYPOGRAPHY from '@token/typography'; | ||
|
||
interface InputWrapperStyleProps { | ||
theme: Theme; | ||
hasFocus: boolean; | ||
hasError: boolean; | ||
} | ||
|
||
interface InputStyleProps { | ||
theme: Theme; | ||
textSize: TextSize; | ||
} | ||
|
||
interface InputSizeStyleProps { | ||
textSize: TextSize; | ||
} | ||
|
||
interface InputBaseStyleProps { | ||
theme: Theme; | ||
} | ||
|
||
export const inputWrapperStyle = ({theme, hasFocus, hasError}: InputWrapperStyleProps) => | ||
css({ | ||
position: 'relative', | ||
display: 'inline-block', | ||
|
||
'&::after': { | ||
content: '""', | ||
position: 'absolute', | ||
left: 0, | ||
right: 0, | ||
bottom: 0, | ||
height: '0.125rem', | ||
backgroundColor: hasFocus ? theme.colors.primary : hasError ? theme.colors.error : 'transparent', | ||
transition: 'background-color 0.2s', | ||
transitionTimingFunction: 'cubic-bezier(0.7, 0.62, 0.62, 1.16)', | ||
}, | ||
}); | ||
|
||
export const inputStyle = ({theme, textSize}: InputStyleProps) => [inputSizeStyle({textSize}), inputBaseStyle({theme})]; | ||
|
||
const inputSizeStyle = ({textSize}: InputSizeStyleProps) => { | ||
const style = { | ||
head: css(TYPOGRAPHY.head), | ||
title: css(TYPOGRAPHY.title), | ||
subTitle: css(TYPOGRAPHY.subTitle), | ||
bodyBold: css(TYPOGRAPHY.bodyBold), | ||
body: css(TYPOGRAPHY.body), | ||
smallBodyBold: css(TYPOGRAPHY.smallBodyBold), | ||
smallBody: css(TYPOGRAPHY.smallBody), | ||
captionBold: css(TYPOGRAPHY.captionBold), | ||
caption: css(TYPOGRAPHY.caption), | ||
tiny: css(TYPOGRAPHY.tiny), | ||
}; | ||
|
||
return [style[textSize]]; | ||
}; | ||
|
||
const inputBaseStyle = ({theme}: InputBaseStyleProps) => | ||
css({ | ||
border: 'none', | ||
outline: 'none', | ||
paddingBottom: '0.125rem', | ||
|
||
color: theme.colors.black, | ||
'&:placeholder': { | ||
color: theme.colors.gray, | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** @jsxImportSource @emotion/react */ | ||
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; | ||
|
||
import {InputProps} from '@components/EditableItem/EditableItem.Input.type'; | ||
|
||
import {useTheme} from '@theme/HDesignProvider'; | ||
|
||
import {inputStyle, inputWrapperStyle} from './EditableItem.Input.style'; | ||
import useEditableItemInput from './useEditableItemInput'; | ||
|
||
export const EditableItemInput: React.FC<InputProps> = forwardRef<HTMLInputElement, InputProps>(function Input( | ||
{textSize = 'body', hasError = false, ...htmlProps}, | ||
ref, | ||
) { | ||
const {theme} = useTheme(); | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
const {hasFocus} = useEditableItemInput({inputRef}); | ||
useImperativeHandle(ref, () => inputRef.current!); | ||
|
||
return ( | ||
<div css={inputWrapperStyle({theme, hasFocus, hasError})}> | ||
<input css={inputStyle({theme, textSize})} ref={inputRef} {...htmlProps} /> | ||
</div> | ||
); | ||
}); | ||
|
||
export default EditableItemInput; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import {TextSize} from '@components/Text/Text.type'; | ||
|
||
import {Theme} from '@theme/theme.type'; | ||
|
||
export interface InputStyleProps { | ||
hasError?: boolean; | ||
textSize?: TextSize; | ||
} | ||
|
||
export interface InputCustomProps {} | ||
|
||
export interface InputStylePropsWithTheme extends InputStyleProps { | ||
theme: Theme; | ||
} | ||
|
||
export type InputOptionProps = InputStyleProps & InputCustomProps; | ||
|
||
export type InputProps = React.ComponentProps<'input'> & InputOptionProps; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** @jsxImportSource @emotion/react */ | ||
import {createContext, PropsWithChildren, useContext, useState} from 'react'; | ||
|
||
interface EditableItemContextProps { | ||
hasAnyFocus: boolean; | ||
setHasAnyFocus: React.Dispatch<React.SetStateAction<boolean>>; | ||
} | ||
|
||
const EditableItemContext = createContext<EditableItemContextProps | null>(null); | ||
|
||
export const useEditableItemContext = () => { | ||
const context = useContext(EditableItemContext); | ||
if (!context) { | ||
throw new Error('useEditableItemContext must be used within an EditableItemProvider'); | ||
} | ||
return context; | ||
}; | ||
|
||
export const EditableItemProvider: React.FC<PropsWithChildren> = ({children}: React.PropsWithChildren) => { | ||
const [hasAnyFocus, setHasAnyFocus] = useState(false); | ||
|
||
return <EditableItemContext.Provider value={{hasAnyFocus, setHasAnyFocus}}>{children}</EditableItemContext.Provider>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/** @jsxImportSource @emotion/react */ | ||
import type {Meta, StoryObj} from '@storybook/react'; | ||
|
||
import EditableItemInput from '@components/EditableItem/EditableItem.Input'; | ||
|
||
import EditableItem from './EditableItem'; | ||
import {EditableItemProvider} from './EditableItem.context'; | ||
|
||
const meta = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스토리북까지 아주 꼼꼼하게 만들어주어서 컴포넌트가 새로 생겨도 빠르게 이해할 수 있었슴다 👍 |
||
title: 'Components/EditableItemInput', | ||
component: EditableItemInput, | ||
tags: ['autodocs'], | ||
parameters: {}, | ||
argTypes: { | ||
textSize: { | ||
description: '', | ||
control: {type: 'select'}, | ||
}, | ||
hasError: { | ||
description: '', | ||
control: {type: 'boolean'}, | ||
}, | ||
}, | ||
args: { | ||
placeholder: '지출 내역', | ||
textSize: 'body', | ||
hasError: false, | ||
autoFocus: true, | ||
}, | ||
} satisfies Meta<typeof EditableItemInput>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Playground: Story = { | ||
render: ({...args}) => { | ||
return ( | ||
<EditableItemProvider> | ||
<EditableItem.Input {...args} /> | ||
</EditableItemProvider> | ||
); | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/** @jsxImportSource @emotion/react */ | ||
import type {Meta, StoryObj} from '@storybook/react'; | ||
|
||
import EditableItem from '@components/EditableItem/EditableItem'; | ||
import Flex from '@components/Flex/Flex'; | ||
import Text from '@components/Text/Text'; | ||
|
||
const meta = { | ||
title: 'Components/EditableItem', | ||
component: EditableItem, | ||
tags: ['autodocs'], | ||
parameters: {}, | ||
argTypes: { | ||
backgroundColor: { | ||
description: '', | ||
control: {type: 'select'}, | ||
}, | ||
}, | ||
args: { | ||
backgroundColor: 'lightGrayContainer', | ||
}, | ||
} satisfies Meta<typeof EditableItem>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Playground: Story = { | ||
render: ({...args}) => { | ||
return ( | ||
<EditableItem | ||
backgroundColor={args.backgroundColor} | ||
onFocus={() => console.log('focus')} | ||
onBlur={() => console.log('blur')} | ||
> | ||
<EditableItem.Input placeholder="지출 내역" textSize="bodyBold"></EditableItem.Input> | ||
<Flex gap="0.25rem" alignItems="center"> | ||
<EditableItem.Input placeholder="0" type="number" style={{textAlign: 'right'}}></EditableItem.Input> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. type이 number일 때, |
||
<Text size="caption">원</Text> | ||
</Flex> | ||
</EditableItem> | ||
); | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {css} from '@emotion/react'; | ||
|
||
import {Theme} from '@theme/theme.type'; | ||
|
||
import {ColorKeys} from '@token/colors'; | ||
|
||
export const editableItemStyle = (theme: Theme, backgroundColor: ColorKeys) => | ||
css({ | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
padding: '0.5rem', | ||
borderRadius: '0.5rem', | ||
backgroundColor: theme.colors[backgroundColor], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** @jsxImportSource @emotion/react */ | ||
import React, {useEffect} from 'react'; | ||
|
||
import {useTheme} from '@theme/HDesignProvider'; | ||
|
||
import {editableItemStyle} from './EditableItem.style'; | ||
import EditableItemInput from './EditableItem.Input'; | ||
import {EditableItemProps} from './EditableItem.type'; | ||
import {EditableItemProvider} from './EditableItem.context'; | ||
import useEditableItem from './useEditableItem'; | ||
|
||
const EditableItemBase = ({ | ||
onInputFocus, | ||
onInputBlur, | ||
backgroundColor = 'white', | ||
children, | ||
...htmlProps | ||
}: EditableItemProps) => { | ||
const {theme} = useTheme(); | ||
|
||
useEditableItem({onInputFocus, onInputBlur}); | ||
|
||
return ( | ||
<div css={editableItemStyle(theme, backgroundColor)} {...htmlProps}> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
export const EditableItem = (props: EditableItemProps) => { | ||
return ( | ||
<EditableItemProvider> | ||
<EditableItemBase {...props} /> | ||
</EditableItemProvider> | ||
); | ||
}; | ||
|
||
EditableItem.Input = EditableItemInput; | ||
|
||
export default EditableItem; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import {Theme} from '@theme/theme.type'; | ||
|
||
import {ColorKeys} from '@token/colors'; | ||
|
||
export interface EditableItemStyleProps { | ||
backgroundColor: ColorKeys; | ||
} | ||
|
||
export interface EditableItemCustomProps { | ||
onInputFocus?: () => void; | ||
onInputBlur?: () => void; | ||
} | ||
|
||
export interface EditableItemStylePropsWithTheme extends EditableItemStyleProps { | ||
theme: Theme; | ||
} | ||
|
||
export type EditableItemOptionProps = EditableItemStyleProps & EditableItemCustomProps; | ||
|
||
export type EditableItemProps = React.ComponentProps<'div'> & EditableItemOptionProps; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import {useEffect} from 'react'; | ||
|
||
import {useEditableItemContext} from './EditableItem.context'; | ||
|
||
interface UseEditableItemProps { | ||
onInputFocus?: () => void; | ||
onInputBlur?: () => void; | ||
} | ||
|
||
const useEditableItem = ({onInputFocus, onInputBlur}: UseEditableItemProps) => { | ||
const {hasAnyFocus} = useEditableItemContext(); | ||
|
||
useEffect(() => { | ||
if (hasAnyFocus && onInputFocus) { | ||
onInputFocus(); | ||
} | ||
if (!hasAnyFocus && onInputBlur) { | ||
onInputBlur(); | ||
} | ||
}, [hasAnyFocus, onInputFocus, onInputBlur]); | ||
}; | ||
Comment on lines
+13
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. focus가 풀리거나 blur될 때 api가 실행되도록 하는 확장성 좋아요! |
||
|
||
export default useEditableItem; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 이건 무슨 뜻인가요!?