-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(ui-kit): Container 컴포넌트 추가 * feat(ui-kit): Tab 컴포넌트 골격 * feat(ui-kit): Tab 스타일 * feat(ui-kit): Tab active 기능 * feat(ui-kit): Tab Bar 애니메이션 추가 * feat(ui-kit): Fixed Width Tab 구현 * feat(ui-kit): Tab Content 구현 * fix(ui-kit): QA 피드백 반영
- Loading branch information
Showing
19 changed files
with
612 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.container { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { createContext } from 'react'; | ||
import { Tab } from './types'; | ||
|
||
export interface TabContextProps { | ||
tabs: Tab[]; | ||
} | ||
|
||
export default createContext<TabContextProps>({ tabs: [] }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import React from 'react'; | ||
|
||
export default function TabNav() { | ||
return <div></div>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import React, { useContext, useState, useEffect, useMemo, useRef } from 'react'; | ||
import classnames from 'classnames'; | ||
import TabNode from './TabNode'; | ||
import TabContext from './TabContext'; | ||
import { TabOffsetMap, TabOffset, TabSizeMap } from './types'; | ||
import { useRefs } from '../../hooks'; | ||
|
||
const DEFAULT_SIZE = { width: 0, height: 0, left: 0, top: 0 }; | ||
|
||
export interface TabNavListProps { | ||
id: string; | ||
activeKey: string; | ||
panes: React.ReactNode; | ||
animated?: boolean; | ||
tabBarGutter?: number; | ||
tabWidth?: number; | ||
onTabClick: (activeKey: string, e: React.MouseEvent) => void; | ||
children?: (node: React.ReactElement) => React.ReactElement; | ||
} | ||
|
||
function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) { | ||
const { tabs } = useContext(TabContext); | ||
const { id, activeKey, animated, tabWidth, onTabClick } = props; | ||
|
||
const tabsWrapperRef = useRef<HTMLDivElement>(); | ||
const tabListRef = useRef<HTMLDivElement>(); | ||
const getTabRef = useRefs<HTMLDivElement>(); | ||
|
||
const [wrapperScrollWidth, setWrapperScrollWidth] = useState<number>(0); | ||
const [wrapperContentWidth, setWrapperContentWidth] = useState<number>(0); | ||
const [wrapperWidth, setWrapperWidth] = useState<number>(0); | ||
const [barStyle, setBarStyle] = useState<React.CSSProperties>(); | ||
const [tabSizes, setTabSizes] = useState<TabSizeMap>(new Map()); | ||
|
||
const tabOffsets = useMemo(() => { | ||
const map: TabOffsetMap = new Map(); | ||
|
||
const lastOffset = tabSizes.get(tabs[0].key) ?? DEFAULT_SIZE; | ||
const rightOffset = lastOffset.left + lastOffset.width; | ||
|
||
for (let i = 0; i < tabs.length; i += 1) { | ||
const { key } = tabs[i]; | ||
let data = tabSizes.get(key); | ||
|
||
if (!data) { | ||
data = tabSizes.get(tabs[i - 1]?.key) || DEFAULT_SIZE; | ||
} | ||
|
||
const entity = (map.get(key) || { ...data }) as TabOffset; | ||
entity.right = rightOffset - entity.left - entity.width; | ||
map.set(key, entity); | ||
} | ||
|
||
return map; | ||
}, [tabs.map((tab) => tab.key).join('_'), tabSizes, wrapperScrollWidth]); | ||
|
||
const activeTabOffset = tabOffsets.get(activeKey); | ||
|
||
useEffect(() => { | ||
const newBarStyle: React.CSSProperties = {}; | ||
|
||
if (activeTabOffset) { | ||
newBarStyle.left = activeTabOffset.left; | ||
newBarStyle.width = tabWidth ? tabWidth : activeTabOffset.width; | ||
} | ||
|
||
setBarStyle(newBarStyle); | ||
}, [activeTabOffset, tabWidth]); | ||
|
||
useEffect(() => { | ||
const offsetWidth = tabsWrapperRef.current?.offsetWidth || 0; | ||
|
||
setWrapperWidth(offsetWidth); | ||
|
||
const newWrapperScrollWidth = tabListRef.current?.offsetWidth || 0; | ||
|
||
setWrapperScrollWidth(newWrapperScrollWidth); | ||
|
||
setWrapperContentWidth(newWrapperScrollWidth); | ||
|
||
setTabSizes(() => { | ||
const newSizes: TabSizeMap = new Map(); | ||
tabs.forEach(({ key }) => { | ||
const tabNode = getTabRef(key).current; | ||
|
||
if (tabNode) { | ||
newSizes.set(key, { | ||
width: tabNode.offsetWidth, | ||
height: tabNode.offsetHeight, | ||
left: tabNode.offsetLeft, | ||
top: tabNode.offsetTop, | ||
}); | ||
} | ||
}); | ||
|
||
return newSizes; | ||
}); | ||
}, []); | ||
|
||
function scrollToTab(key = activeKey) {} | ||
|
||
const tabNodes: React.ReactElement[] = tabs.map((tab) => { | ||
const { key } = tab; | ||
return ( | ||
<TabNode | ||
id={id} | ||
tab={tab} | ||
key={key} | ||
ref={getTabRef(key)} | ||
tabWidth={tabWidth} | ||
active={key === activeKey} | ||
onClick={(e) => { | ||
onTabClick(key, e); | ||
}} | ||
onFocus={() => { | ||
scrollToTab(key); | ||
|
||
tabsWrapperRef.current.scrollToLeft = 0; | ||
}} | ||
/> | ||
); | ||
}); | ||
|
||
return ( | ||
<div ref={ref} role="tablist" className={classnames('lubycon-tab__nav')}> | ||
<div ref={tabsWrapperRef} className={classnames('lubycon-tab__nav__wrap')}> | ||
<div ref={tabListRef} className="lubycon-nav-list"> | ||
{tabNodes} | ||
</div> | ||
|
||
<div | ||
className={classnames(`lubycon-tab__bar`, { | ||
['lubycon-tab__bar__animated']: animated ? animated : true, | ||
})} | ||
style={barStyle} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default React.forwardRef(TabNavList); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import * as React from 'react'; | ||
import classNames from 'classnames'; | ||
import { Tab } from './types'; | ||
|
||
export interface TabNodeProps { | ||
id: string; | ||
tab: Tab; | ||
active: boolean; | ||
tabWidth?: number; | ||
onClick?: (e: React.MouseEvent) => void; | ||
onFocus: React.FocusEventHandler; | ||
} | ||
|
||
function TabNode( | ||
{ id, active, tab: { key, tab, disabled }, tabWidth, onClick, onFocus }: TabNodeProps, | ||
ref: React.Ref<HTMLDivElement> | ||
) { | ||
const tabPrefix = 'lubycon-tab'; | ||
|
||
const nodeStyle: React.CSSProperties = { width: tabWidth }; | ||
|
||
function onInternalClick(e: React.MouseEvent) { | ||
if (disabled) { | ||
return; | ||
} | ||
|
||
onClick?.(e); | ||
} | ||
|
||
return ( | ||
<div | ||
key={key} | ||
ref={ref} | ||
className={classNames(tabPrefix, { | ||
[`${tabPrefix}__active`]: active, | ||
[`${tabPrefix}__disabled`]: disabled, | ||
})} | ||
style={nodeStyle} | ||
onClick={onInternalClick} | ||
> | ||
<div | ||
role="tab" | ||
aria-selected={active} | ||
id={id !== null ? `${id}-tab-${key}` : ''} | ||
className={`${tabPrefix}__btn`} | ||
aria-controls={id !== null ? `${id}-panel-${key}` : ''} | ||
aria-disabled={disabled} | ||
tabIndex={disabled ? undefined : 0} | ||
onClick={(e) => { | ||
e.stopPropagation(); | ||
onInternalClick(e); | ||
}} | ||
onFocus={onFocus} | ||
> | ||
{tab} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default React.forwardRef(TabNode); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from 'react'; | ||
import classnames from 'classnames'; | ||
|
||
export interface TabPaneProps { | ||
tab?: React.ReactNode; | ||
children?: React.ReactNode; | ||
active?: boolean; | ||
animated?: boolean; | ||
disabled?: boolean; | ||
} | ||
|
||
export default function TabPane({ active, animated, children }: TabPaneProps) { | ||
const mergedStyle: React.CSSProperties = {}; | ||
if (!active) { | ||
if (animated) { | ||
mergedStyle.visibility = 'hidden'; | ||
mergedStyle.height = 0; | ||
mergedStyle.overflowY = 'hidden'; | ||
} else { | ||
mergedStyle.display = 'none'; | ||
} | ||
} | ||
|
||
return ( | ||
<div | ||
role="tabpanel" | ||
tabIndex={active ? 0 : -1} | ||
aria-hidden={!active} | ||
className={classnames('lubycon-tab__pane', active && `lubycon-tab__pane__active`)} | ||
style={{ ...mergedStyle }} | ||
> | ||
{active && children} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React from 'react'; | ||
import classnames from 'classnames'; | ||
import TabContext from './TabContext'; | ||
|
||
export interface TabPanelListProps { | ||
activeKey: React.Key; | ||
animated?: boolean; | ||
} | ||
|
||
export default function TabPanelList({ activeKey, animated }: TabPanelListProps) { | ||
const { tabs } = React.useContext(TabContext); | ||
const tabPaneAnimated = animated; | ||
|
||
const activeIndex = tabs.findIndex((tab) => tab.key === activeKey); | ||
|
||
return ( | ||
<div className={classnames('lubycon-tab__content__holder')}> | ||
<div | ||
className={classnames('lubycon-tab__content', { | ||
['lubycon-tab__content__animated']: tabPaneAnimated, | ||
})} | ||
style={ | ||
activeIndex && tabPaneAnimated ? { ['marginLeft']: `-${activeIndex}00%` } : undefined | ||
} | ||
> | ||
{tabs.map((tab) => { | ||
return React.cloneElement(tab.node, { | ||
key: tab.key, | ||
tabKey: tab.key, | ||
animated: tabPaneAnimated, | ||
active: tab.key === activeKey, | ||
}); | ||
})} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import React from 'react'; | ||
import classnames from 'classnames'; | ||
import TabPane, { TabPaneProps } from './TabPane'; | ||
import TabNavList from './TabNavList'; | ||
import { Tab } from './types'; | ||
import { useMergedState } from '../../hooks'; | ||
import { toArray } from '../../utils'; | ||
import TabContext from './TabContext'; | ||
import TabPanelList from './TabPanelList'; | ||
|
||
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> { | ||
children?: React.ReactNode; | ||
activeKey?: string; | ||
defaultActiveKey?: string; | ||
animated?: boolean; | ||
tabWidth?: number; | ||
onTabClick?: (activeKey: string, e: React.MouseEvent) => void; | ||
onChange?: (activeKey: string) => void; | ||
} | ||
|
||
function parseTabList(children: React.ReactNode): Tab[] { | ||
return toArray(children) | ||
.map((node: React.ReactElement<TabPaneProps>) => { | ||
if (React.isValidElement(node)) { | ||
const key = node.key !== undefined ? String(node.key) : undefined; | ||
return { | ||
key, | ||
...node.props, | ||
node, | ||
}; | ||
} | ||
|
||
return null; | ||
}) | ||
.filter((tab) => tab); | ||
} | ||
|
||
function Tabs( | ||
{ children, activeKey, defaultActiveKey, animated, tabWidth, onTabClick, onChange }: TabsProps, | ||
ref: React.Ref<HTMLDivElement> | ||
) { | ||
const tabs = parseTabList(children); | ||
|
||
const [mergedActiveKey, setMergedActiveKey] = useMergedState<string>(() => tabs[0]?.key, { | ||
value: activeKey, | ||
defaultValue: defaultActiveKey, | ||
}); | ||
|
||
function onInternalTabClick(key: string, e: React.MouseEvent) { | ||
onTabClick?.(key, e); | ||
|
||
setMergedActiveKey(key); | ||
onChange?.(key); | ||
} | ||
|
||
const tabNavBarProps = { | ||
id: '', | ||
activeKey: mergedActiveKey, | ||
animated, | ||
tabWidth, | ||
panes: children, | ||
onTabClick: onInternalTabClick, | ||
}; | ||
|
||
const tabNavBar: React.ReactElement = <TabNavList {...tabNavBarProps} />; | ||
|
||
return ( | ||
<TabContext.Provider value={{ tabs }}> | ||
<div ref={ref} className={classnames('lubycon-tabs')}> | ||
{tabNavBar} | ||
|
||
<TabPanelList activeKey={mergedActiveKey} animated={animated} /> | ||
</div> | ||
</TabContext.Provider> | ||
); | ||
} | ||
|
||
const ForwardTabs = React.forwardRef(Tabs); | ||
|
||
export type ForwardTabsType = typeof ForwardTabs & { TabPane: typeof TabPane }; | ||
|
||
(ForwardTabs as ForwardTabsType).TabPane = TabPane; | ||
|
||
export default ForwardTabs as ForwardTabsType; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default as Tabs } from './Tabs'; | ||
export { default as TabPane } from './TabPane'; |
Oops, something went wrong.