-
Notifications
You must be signed in to change notification settings - Fork 0
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
사이드바 구현 #76
base: dev
Are you sure you want to change the base?
사이드바 구현 #76
Changes from all commits
d891a2a
ddfa546
4258290
aece736
4d8f828
b9d2294
ca1d915
c303edd
10e29c1
4d2d607
0a8d097
d5ce487
d01eadd
1c3d672
278ffbe
c1cfb8e
3d843aa
d6c9cf6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
declare module '*.jpg'; | ||
declare module '*.png'; | ||
declare module '*.svg'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import React from 'react'; | ||
import { MemoryRouter, Router } from 'react-router-dom'; | ||
import { createMemoryHistory } from 'history'; | ||
|
||
import { render, screen, fireEvent } from '@testing-library/react'; | ||
|
||
import IconTabBar from '.'; | ||
|
||
describe('<IconTabBar/> component test', () => { | ||
it('탭 요소들을 렌더링 해야 합니다.', () => { | ||
render( | ||
<MemoryRouter> | ||
<IconTabBar> | ||
<IconTabBar.TabItem>ITEM 1</IconTabBar.TabItem> | ||
<IconTabBar.TabItem>ITEM 2</IconTabBar.TabItem> | ||
<IconTabBar.TabItem>ITEM 3</IconTabBar.TabItem> | ||
<IconTabBar.TabIcon src="#">ICON 1</IconTabBar.TabIcon> | ||
<IconTabBar.TabIcon src="#">ICON 2</IconTabBar.TabIcon> | ||
<IconTabBar.TabIcon src="#">ICON 3</IconTabBar.TabIcon> | ||
</IconTabBar> | ||
</MemoryRouter>, | ||
); | ||
|
||
expect(screen.queryByText('ITEM 1')).toBeInTheDocument(); | ||
expect(screen.queryByText('ITEM 2')).toBeInTheDocument(); | ||
expect(screen.queryByText('ITEM 3')).toBeInTheDocument(); | ||
|
||
expect(screen.queryByAltText('ICON 1')).toBeInTheDocument(); | ||
expect(screen.queryByAltText('ICON 2')).toBeInTheDocument(); | ||
expect(screen.queryByAltText('ICON 3')).toBeInTheDocument(); | ||
}); | ||
|
||
describe('<IconTabBar.TabItem/> component test', () => { | ||
it('자식 요소를 렌더링 해야 합니다.', () => { | ||
render( | ||
<MemoryRouter> | ||
<IconTabBar.TabItem>Content</IconTabBar.TabItem> | ||
</MemoryRouter>, | ||
); | ||
|
||
expect(screen.queryByText('Content')).toBeInTheDocument(); | ||
}); | ||
|
||
it('함수로 전달된 자식 요소에 URL을 기반으로한 선택 여부를 전달해야 합니다.', () => { | ||
render( | ||
<MemoryRouter initialEntries={['/target']}> | ||
<IconTabBar.TabItem to="/target"> | ||
{(selected) => selected && 'TRUE'} | ||
</IconTabBar.TabItem> | ||
</MemoryRouter>, | ||
); | ||
|
||
expect(screen.queryByText('TRUE')).toBeInTheDocument(); | ||
}); | ||
|
||
it('클릭 핸들러가 지정된 탭 요소를 클릭하면 해당 클릭 핸들러가 호출되어야 합니다.', () => { | ||
let isClickHandlerCalled = false; | ||
|
||
render( | ||
<MemoryRouter> | ||
<IconTabBar.TabItem | ||
onClickHandler={() => { | ||
isClickHandlerCalled = true; | ||
}} | ||
> | ||
ITEM | ||
</IconTabBar.TabItem> | ||
</MemoryRouter>, | ||
); | ||
|
||
fireEvent.click(screen.getByText('ITEM')); | ||
|
||
expect(isClickHandlerCalled).toBeTruthy(); | ||
}); | ||
|
||
it('링크 URL이 지정된 탭 요소를 클릭하면 해당 URL로 이동해야 합니다.', () => { | ||
const history = createMemoryHistory(); | ||
|
||
render( | ||
<Router history={history}> | ||
<IconTabBar.TabItem to="/target">ITEM</IconTabBar.TabItem> | ||
</Router>, | ||
); | ||
|
||
fireEvent.click(screen.getByText('ITEM')); | ||
|
||
expect(history.location.pathname).toBe('/target'); | ||
}); | ||
}); | ||
|
||
describe('<IconTabBar.TabIcon/> component test', () => { | ||
const DEFAULT_ALT_TEXT = 'icon'; | ||
|
||
it('클릭 핸들러가 지정된 아이콘을 클릭하면 해당 클릭 핸들러가 호출되어야 합니다.', () => { | ||
let isClickHandlerCalled = false; | ||
|
||
render( | ||
<MemoryRouter> | ||
<IconTabBar.TabIcon | ||
src="#" | ||
onClickHandler={() => { | ||
isClickHandlerCalled = true; | ||
}} | ||
/> | ||
</MemoryRouter>, | ||
); | ||
|
||
fireEvent.click(screen.getByRole('img', { name: DEFAULT_ALT_TEXT })); | ||
|
||
expect(isClickHandlerCalled).toBeTruthy(); | ||
}); | ||
|
||
it('링크 URL이 지정된 아이콘을 클릭하면 해당 URL로 이동해야 합니다.', () => { | ||
const history = createMemoryHistory(); | ||
|
||
render( | ||
<Router history={history}> | ||
<IconTabBar.TabIcon src="#" to="/target" /> | ||
</Router>, | ||
); | ||
|
||
fireEvent.click(screen.getByRole('img', { name: DEFAULT_ALT_TEXT })); | ||
|
||
expect(history.location.pathname).toBe('/target'); | ||
}); | ||
|
||
it('아이콘 이미지의 대체 텍스트를 지정할 수 있어야 합니다.', () => { | ||
render( | ||
<MemoryRouter> | ||
<IconTabBar.TabIcon src="#" to="/target"> | ||
Icon Alt Text | ||
</IconTabBar.TabIcon> | ||
</MemoryRouter>, | ||
); | ||
|
||
expect(screen.queryByAltText('Icon Alt Text')).toBeInTheDocument(); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,118 @@ | ||||||||||||||||||||
import { Theme } from '@emotion/react'; | ||||||||||||||||||||
import React, { FC, MouseEventHandler } from 'react'; | ||||||||||||||||||||
import { useHistory, useLocation } from 'react-router-dom'; | ||||||||||||||||||||
|
||||||||||||||||||||
import { | ||||||||||||||||||||
tabBarStyle, | ||||||||||||||||||||
tabIconStyle, | ||||||||||||||||||||
tabItemStyle, | ||||||||||||||||||||
tabCurvedBlockStyle, | ||||||||||||||||||||
} from './style'; | ||||||||||||||||||||
|
||||||||||||||||||||
type TabItemProps = { | ||||||||||||||||||||
to?: string; | ||||||||||||||||||||
onClickHandler?: MouseEventHandler<HTMLButtonElement>; | ||||||||||||||||||||
children: React.ReactNode | ((selected: boolean) => React.ReactNode); | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
const TabItem: FC<TabItemProps> = ({ children, to, onClickHandler }) => { | ||||||||||||||||||||
const history = useHistory(); | ||||||||||||||||||||
const location = useLocation(); | ||||||||||||||||||||
|
||||||||||||||||||||
const onClickTabItem: MouseEventHandler<HTMLButtonElement> = (e) => { | ||||||||||||||||||||
if (to) { | ||||||||||||||||||||
history.push(to); | ||||||||||||||||||||
} | ||||||||||||||||||||
if (onClickHandler) { | ||||||||||||||||||||
onClickHandler(e); | ||||||||||||||||||||
} | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
return ( | ||||||||||||||||||||
<button | ||||||||||||||||||||
type="button" | ||||||||||||||||||||
className="tab-item" | ||||||||||||||||||||
css={tabItemStyle} | ||||||||||||||||||||
onClick={onClickTabItem} | ||||||||||||||||||||
> | ||||||||||||||||||||
{typeof children === 'function' | ||||||||||||||||||||
? children(to === location.pathname) | ||||||||||||||||||||
: children} | ||||||||||||||||||||
</button> | ||||||||||||||||||||
); | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
TabItem.defaultProps = { | ||||||||||||||||||||
to: null, | ||||||||||||||||||||
onClickHandler: null, | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
type TabIconProps = { | ||||||||||||||||||||
src: string; | ||||||||||||||||||||
children?: string; | ||||||||||||||||||||
selectable?: boolean; | ||||||||||||||||||||
} & Omit<TabItemProps, 'children'>; | ||||||||||||||||||||
|
||||||||||||||||||||
const TabIcon: FC<TabIconProps> = ({ | ||||||||||||||||||||
children, | ||||||||||||||||||||
src, | ||||||||||||||||||||
to, | ||||||||||||||||||||
selectable, | ||||||||||||||||||||
onClickHandler, | ||||||||||||||||||||
}) => { | ||||||||||||||||||||
return ( | ||||||||||||||||||||
<TabItem to={to} onClickHandler={onClickHandler}> | ||||||||||||||||||||
{(selected) => ( | ||||||||||||||||||||
<div | ||||||||||||||||||||
css={tabCurvedBlockStyle} | ||||||||||||||||||||
className={`curved-block-wrap ${ | ||||||||||||||||||||
!selected || !selectable ? 'not-selected' : '' | ||||||||||||||||||||
}`} | ||||||||||||||||||||
> | ||||||||||||||||||||
<div className="curved-block start" /> | ||||||||||||||||||||
<div | ||||||||||||||||||||
css={tabIconStyle} | ||||||||||||||||||||
className={`tab-icon ${selected && selectable ? 'selected' : ''}`} | ||||||||||||||||||||
> | ||||||||||||||||||||
<img src={src} alt={children} /> | ||||||||||||||||||||
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. alt 를 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. 넹 제 생각도 children으로 안 받고 이미지 alt를 이 이미지 역할 정도로만 정의해줘도 될 것 같아요! 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. TabIcon이라는 네이밍에서 이것은 아이콘만을 보여주는 컴포넌트라는 것을 드러냈다면 굳이 alt와 같이 이미지 태그스러운 속성을 사용할 필요가 있을까 했습니다. 그래서 children으로 받아 일종의 숨김을 사용한 것이었는 데 두 분의 의견이 직관적이지 않다로 일치하신 것 같아 alt 속성을 사용한 형태로 수정수정,,,, |
||||||||||||||||||||
</div> | ||||||||||||||||||||
<div className="curved-block end" /> | ||||||||||||||||||||
</div> | ||||||||||||||||||||
)} | ||||||||||||||||||||
</TabItem> | ||||||||||||||||||||
); | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
TabIcon.defaultProps = { | ||||||||||||||||||||
children: 'icon', | ||||||||||||||||||||
selectable: true, | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
type IconTabBarProps = { | ||||||||||||||||||||
direction?: 'top' | 'right' | 'bottom' | 'left'; | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
const IconTabBar: FC<IconTabBarProps> & { | ||||||||||||||||||||
TabIcon: FC<TabIconProps>; | ||||||||||||||||||||
TabItem: FC<TabItemProps>; | ||||||||||||||||||||
} = ({ children, direction }) => { | ||||||||||||||||||||
const isRow = direction === 'top' || direction === 'bottom'; | ||||||||||||||||||||
|
||||||||||||||||||||
return ( | ||||||||||||||||||||
<div | ||||||||||||||||||||
className={direction} | ||||||||||||||||||||
css={(cssTheme: Theme) => tabBarStyle(cssTheme, isRow)} | ||||||||||||||||||||
> | ||||||||||||||||||||
{children} | ||||||||||||||||||||
</div> | ||||||||||||||||||||
); | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
IconTabBar.defaultProps = { | ||||||||||||||||||||
direction: 'right', | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
IconTabBar.TabIcon = TabIcon; | ||||||||||||||||||||
IconTabBar.TabItem = TabItem; | ||||||||||||||||||||
Comment on lines
+115
to
+116
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. 확실히 Compound Components 를 사용하니 훨씬 수정하기 용이하게 느껴집니다!!
Suggested change
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. 좋습니당 |
||||||||||||||||||||
|
||||||||||||||||||||
export default IconTabBar; |
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.
👍🏻👍🏻 ServiceEditPage 라우터는 나중에 올라올 서비스 수정 API 연동 PR에서 수정되었습니닷!!!