Skip to content

Commit d898816

Browse files
authored
v0.2.12
2 parents 33cc145 + 9e60f88 commit d898816

28 files changed

+792
-39
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import base from 'paths.macro'
4+
5+
/* Internal dependencies */
6+
import { Navigation } from '../../../layout/Navigation'
7+
import { getTitle } from '../../../utils/utils'
8+
import ListItem from './ListItem'
9+
10+
export default {
11+
title: getTitle(base),
12+
component: ListItem,
13+
argTypes: {
14+
onClick: { control: { action: 'onClick' } },
15+
active: { control: { type: 'boolean' } },
16+
},
17+
}
18+
19+
const SIDEBAR_WIDTH = 240
20+
21+
const Template = ({ ...otherListItemProps }) => (
22+
<Navigation
23+
withScroll
24+
disableResize
25+
title="사이드바"
26+
minWidth={SIDEBAR_WIDTH}
27+
>
28+
<ListItem
29+
optionKey="menu-item-0"
30+
{...otherListItemProps}
31+
/>
32+
</Navigation>
33+
)
34+
35+
export const Primary = Template.bind({})
36+
37+
Primary.args = {
38+
content: '전체 상태',
39+
active: false,
40+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* Internal dependencies */
2+
import { css, styled } from '../../../styling/Theme'
3+
import Palette from '../../../styling/Palette'
4+
import { StyledWrapperProps } from './ListItem.types'
5+
6+
const ActiveItemStyle = css<StyledWrapperProps>`
7+
color: ${Palette.blue500};
8+
background-color: ${Palette.blue100};
9+
`
10+
11+
export const Wrapper = styled.div<StyledWrapperProps>`
12+
display: flex;
13+
align-items: center;
14+
height: 32px;
15+
padding: 0 8px;
16+
margin-right: 6px;
17+
margin-left: 6px;
18+
font-size: 14px;
19+
font-weight: normal;
20+
color: ${props => props.theme?.colors?.text7};
21+
text-decoration: none;
22+
cursor: pointer;
23+
border-radius: 6px;
24+
25+
&:hover {
26+
${props => (props.active ? '' : `
27+
background-color: ${props.theme?.colors?.background3};
28+
`)}
29+
}
30+
31+
${props => (props.active && ActiveItemStyle)}
32+
`
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import { render } from '@testing-library/react'
4+
5+
/* Internal dependencies */
6+
import ListItem, { SIDEBAR_MENU_ITEM_TEST_ID } from './ListItem'
7+
import ListItemProps from './ListItem.types'
8+
9+
describe('ListItem', () => {
10+
let props: ListItemProps
11+
12+
beforeEach(() => {
13+
props = {
14+
content: 'this is content',
15+
optionKey: 'menu-item',
16+
active: false,
17+
}
18+
})
19+
20+
const renderComponent = (optionProps?: Partial<ListItemProps>) => render(
21+
<ListItem {...props} {...optionProps} />,
22+
)
23+
24+
it('should have "optionKey" value on "data-option-key" ', () => {
25+
const { getByTestId } = renderComponent({ optionKey: 'my-menu-item' })
26+
const rendered = getByTestId(SIDEBAR_MENU_ITEM_TEST_ID)
27+
28+
expect(rendered).toHaveAttribute('data-option-key', 'my-menu-item')
29+
})
30+
31+
it('should have "data-active" attribute when "active" prop is "true', () => {
32+
const { getByTestId } = renderComponent({ active: true })
33+
const rendered = getByTestId(SIDEBAR_MENU_ITEM_TEST_ID)
34+
35+
expect(rendered).toHaveAttribute('data-active', 'true')
36+
})
37+
38+
it('should have "a tag" related attributes when "href" prop is string', () => {
39+
const { getByTestId } = renderComponent({ href: 'https://naver.com' })
40+
const rendered = getByTestId(SIDEBAR_MENU_ITEM_TEST_ID)
41+
expect(rendered).toHaveAttribute('href', 'https://naver.com')
42+
expect(rendered).toHaveAttribute('rel', 'noopener noreferer')
43+
expect(rendered).toHaveAttribute('target', '_blank')
44+
})
45+
})
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* External dependencies */
2+
import React, { Ref, forwardRef, useCallback, useMemo } from 'react'
3+
import { get, noop, isNil } from 'lodash-es'
4+
5+
/* Internal dependencies */
6+
import { mergeClassNames } from '../../../utils/stringUtils'
7+
import ListItemProps from './ListItem.types'
8+
import { Wrapper } from './ListItem.styled'
9+
10+
export const SIDEBAR_MENU_ITEM_COMPONENT_NAME = 'ListItem'
11+
export const SIDEBAR_MENU_ITEM_TEST_ID = 'ch-design-system-sidebar-menu-item'
12+
13+
export function isListItem(element: any): element is React.ReactElement<ListItemProps> {
14+
return React.isValidElement(element) &&
15+
get(element, 'type.displayName') === SIDEBAR_MENU_ITEM_COMPONENT_NAME
16+
}
17+
18+
function ListItemComponent({
19+
as,
20+
testId = SIDEBAR_MENU_ITEM_TEST_ID,
21+
content,
22+
href,
23+
hide,
24+
/* OptionItem Props */
25+
optionKey,
26+
/* Activable Element Props */
27+
active = false,
28+
activeClassName,
29+
/* HTMLAttribute Props */
30+
onClick = noop,
31+
className,
32+
...othreProps
33+
}: ListItemProps, forwardedRef: Ref<any>) {
34+
const clazzName = useMemo(() => (
35+
mergeClassNames(className, ((active && activeClassName) || undefined))
36+
), [
37+
className,
38+
activeClassName,
39+
active,
40+
])
41+
42+
const handleClick = useCallback((e) => {
43+
if (!active) {
44+
onClick(e)
45+
}
46+
}, [active, onClick])
47+
48+
if (hide) return null
49+
50+
if (!isNil(href)) {
51+
return (
52+
<Wrapper
53+
ref={forwardedRef}
54+
as="a"
55+
className={clazzName}
56+
draggable={false}
57+
href={href}
58+
target="_blank"
59+
rel="noopener noreferer"
60+
onClick={handleClick}
61+
active={active}
62+
data-active={active}
63+
data-option-key={optionKey}
64+
data-testid={testId}
65+
{...othreProps}
66+
>
67+
{ content }
68+
</Wrapper>
69+
)
70+
}
71+
72+
return (
73+
<Wrapper
74+
as={as}
75+
className={clazzName}
76+
onClick={handleClick}
77+
active={active}
78+
data-active={active}
79+
data-option-key={optionKey}
80+
data-testid={testId}
81+
{...othreProps}
82+
>
83+
{ content }
84+
</Wrapper>
85+
)
86+
}
87+
88+
const ListItem = forwardRef(ListItemComponent)
89+
ListItem.displayName = SIDEBAR_MENU_ITEM_COMPONENT_NAME
90+
91+
export default ListItem
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* Internal dependencies */
2+
import ActivableElement from '../../../types/ActivatableElement'
3+
import { ContentComponentProps, UIComponentProps } from '../../../types/ComponentProps'
4+
import OptionItem from '../../../types/OptionItem'
5+
6+
export default interface ListItemProps extends ContentComponentProps, OptionItem, ActivableElement {
7+
href?: string
8+
hide?: boolean
9+
}
10+
11+
export interface StyledWrapperProps extends UIComponentProps, OptionItem, ActivableElement {}

src/components/List/ListItem/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import ListItem from './ListItem'
2+
import type ListItemProps from './ListItem.types'
3+
4+
export type {
5+
ListItemProps,
6+
}
7+
8+
export {
9+
ListItem,
10+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import base from 'paths.macro'
4+
import { v4 as uuid } from 'uuid'
5+
import { range } from 'lodash-es'
6+
7+
/* Internal dependencies */
8+
import { Navigation } from '../../../layout/Navigation'
9+
import { getTitle } from '../../../utils/utils'
10+
import { ListItem } from '../ListItem'
11+
import ListMenuGroup from './ListMenuGroup'
12+
13+
export default {
14+
title: getTitle(base),
15+
component: ListMenuGroup,
16+
argTypes: {
17+
open: {
18+
control: {
19+
type: 'boolean',
20+
},
21+
},
22+
},
23+
}
24+
25+
const SIDEBAR_WIDTH = 240
26+
27+
const Template = ({ ...otherListMenuGroupProps }) => (
28+
<Navigation
29+
withScroll
30+
disableResize
31+
title="사이드바"
32+
minWidth={SIDEBAR_WIDTH}
33+
>
34+
<ListMenuGroup
35+
{...otherListMenuGroupProps}
36+
>
37+
{ range(0, 4).map(n => (
38+
<ListItem
39+
key={uuid()}
40+
optionKey={`menu-item-${n}`}
41+
content={`아이템 ${n}`}
42+
/>
43+
)) }
44+
</ListMenuGroup>
45+
</Navigation>
46+
)
47+
48+
export const Primary = Template.bind({})
49+
50+
Primary.args = {
51+
content: '전체 상태',
52+
leftIcon: 'sent',
53+
selectedOptionIndex: null,
54+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* External dependencies */
2+
import { isNil } from 'lodash-es'
3+
4+
/* Internal dependencies */
5+
import { styled } from '../../../styling/Theme'
6+
import { StyledWrapperProps } from './ListMenuGroup.types'
7+
8+
export const GroupItemWrapper = styled.div<StyledWrapperProps>`
9+
display: flex;
10+
align-items: center;
11+
height: 32px;
12+
padding: 0 8px;
13+
margin-right: 6px;
14+
margin-left: 6px;
15+
font-size: 14px;
16+
font-weight: normal;
17+
color: ${props => props.theme?.colors?.text7};
18+
text-decoration: none;
19+
cursor: pointer;
20+
border-radius: 6px;
21+
22+
&:hover {
23+
background-color: ${props => props.theme?.colors?.background3};
24+
}
25+
26+
${props => !isNil(props.currentMenuItemIndex) && `
27+
color: ${props.theme?.colors?.focus5};
28+
background-color: ${props.theme?.colors?.background2};
29+
`}
30+
`
31+
32+
export const GroupItemContentWrapper = styled.div`
33+
display: flex;
34+
flex: 1;
35+
align-items: center;
36+
`
37+
38+
export const ChildrenWrapper = styled.div`
39+
& > * {
40+
padding-left: 42px;
41+
}
42+
`
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import { fireEvent, render, screen } from '@testing-library/react'
4+
import { v4 as uuid } from 'uuid'
5+
import { range } from 'lodash-es'
6+
7+
/* Internal dependencies */
8+
import { ListItem } from '../ListItem'
9+
import ListMenuGroup, { SIDEBAR_MENU_GROUP_TEST_ID } from './ListMenuGroup'
10+
import ListMenuGroupProps from './ListMenuGroup.types'
11+
12+
describe('ListMenuGroup', () => {
13+
let props: ListMenuGroupProps
14+
15+
beforeEach(() => {
16+
props = {
17+
open: true,
18+
selectedOptionIndex: 0,
19+
content: 'campaigns',
20+
}
21+
})
22+
23+
const renderComponent = (optionProps?: Partial<ListMenuGroupProps>) => render(
24+
<ListMenuGroup {...props} {...optionProps}>
25+
{ range(0, 4).map(n => (
26+
<ListItem
27+
key={uuid()}
28+
optionKey={`menu-item-${n}`}
29+
content={`item ${n}`}
30+
/>
31+
)) }
32+
</ListMenuGroup>,
33+
)
34+
35+
it('should have default styles', () => {
36+
const { getByTestId } = renderComponent()
37+
const rendered = getByTestId(SIDEBAR_MENU_GROUP_TEST_ID)
38+
39+
expect(rendered).toHaveStyle('display: flex;')
40+
expect(rendered).toHaveStyle('align-items: center;')
41+
expect(rendered).toHaveStyle('height: 32px;')
42+
})
43+
44+
it(
45+
'should have index on "data-active-index" attr when "selectedOptionIndex" given',
46+
() => {
47+
const { getByTestId } = renderComponent({ selectedMenuItemIndex: 2 })
48+
const rendered = getByTestId(SIDEBAR_MENU_GROUP_TEST_ID)
49+
50+
expect(rendered).toHaveAttribute('data-active-index', '2')
51+
})
52+
53+
it('should change "data-active-index"', () => {
54+
const { getByTestId } = renderComponent({ selectedMenuItemIndex: 1 })
55+
const rendered = getByTestId(SIDEBAR_MENU_GROUP_TEST_ID)
56+
57+
expect(rendered).toHaveAttribute('data-active-index', '1')
58+
fireEvent.click(screen.getByText('item 3'))
59+
expect(rendered).toHaveAttribute('data-active-index', '3')
60+
})
61+
})

0 commit comments

Comments
 (0)