diff --git a/applications/launchpad_v2/src/components/Tabs/Tabs.test.tsx b/applications/launchpad_v2/src/components/Tabs/Tabs.test.tsx new file mode 100644 index 00000000000..7583c74a983 --- /dev/null +++ b/applications/launchpad_v2/src/components/Tabs/Tabs.test.tsx @@ -0,0 +1,34 @@ +import { act, render, screen } from '@testing-library/react' +import { ThemeProvider } from 'styled-components' + +import themes from '../../styles/themes' +import Tabs from './' + +const tabs = [ + { + id: 'first-tab', + content: First tab, + }, + { + id: 'second-tab', + content: Second tab, + }, +] + +describe('Tabs', () => { + it('should render without crashing', async () => { + const selected = 'second-tab' + const onSelect = jest.fn() + + await act(async () => { + render( + + + , + ) + }) + + const firstTabText = screen.queryAllByText('First tab') + expect(firstTabText.length).toBeGreaterThan(0) + }) +}) diff --git a/applications/launchpad_v2/src/components/Tabs/index.tsx b/applications/launchpad_v2/src/components/Tabs/index.tsx index 8246f50227c..fb774a6ffd0 100644 --- a/applications/launchpad_v2/src/components/Tabs/index.tsx +++ b/applications/launchpad_v2/src/components/Tabs/index.tsx @@ -1,20 +1,105 @@ -import { TabsContainer, Tab, TabOptions } from './styles' +import { useEffect, useRef, useState } from 'react' +import { useSpring } from 'react-spring' + +import Text from '../Text' + +import { + TabsContainer, + Tab, + TabOptions, + TabContent, + TabSelectedBorder, + FontWeightCompensation, +} from './styles' import { TabsProps } from './types' +/** + * Tabs component + * + * @TODO add details on how to compose the tab content to make the 'selected' styling work properly + * + * @param {TabProp[]} tabs - the list of tabs. + * @param {string} selected - the id of the selected tab. It has to match the `id` prop of the tab. + * @param {(val: string) => void} onSelect - on tab click. + * + * @typedef TabProp + * @param {string} id - unique identifier of the tab + * @param {ReactNode} content - the tab header content + */ const Tabs = ({ tabs, selected, onSelect }: TabsProps) => { + const tabsRefs = useRef<(HTMLButtonElement | null)[]>([]) + // Needs to re-render the component twice on the initial mount, + // because on first rendering, it adds tabs to the DOM, and on + // the second render, it can calculate correct values for bolded fonts. + // So the `initialized` is an integer incremented from 0 to 2 + const [initialized, setInitialzed] = useState(0) + + useEffect(() => { + tabsRefs.current = tabsRefs.current.slice(0, tabs.length) + }, [tabs]) + + useEffect(() => { + if (initialized < 2) { + setInitialzed(initialized + 1) + } + }, [initialized]) + + const selectedIndex = tabs.findIndex(t => t.id === selected) + let width = 0 + let left = 0 + let totalWidth = 0 + + if (selectedIndex > -1) { + if ( + tabsRefs && + tabsRefs.current && + tabsRefs.current.length > selectedIndex + ) { + tabsRefs.current.forEach((el, index) => { + if (el) { + if (index < selectedIndex) { + left = left + el.offsetWidth + } else if (index === selectedIndex) { + width = el.offsetWidth + } + totalWidth = totalWidth + el.offsetWidth + } + }) + } + } + + const activeBorder = useSpring({ + to: { left: left, width: width }, + config: { duration: 100 }, + }) + return ( {tabs.map((tab, index) => ( (tabsRefs.current[index] = el)} onClick={() => onSelect(tab.id)} > - {tab.content} + + {tab.content} + + + + {tab.content} + + ))} + ) } diff --git a/applications/launchpad_v2/src/components/Tabs/styles.ts b/applications/launchpad_v2/src/components/Tabs/styles.ts index d9814319933..b21193bddec 100644 --- a/applications/launchpad_v2/src/components/Tabs/styles.ts +++ b/applications/launchpad_v2/src/components/Tabs/styles.ts @@ -1,31 +1,53 @@ +import { animated } from 'react-spring' import styled from 'styled-components' export const TabsContainer = styled.div` display: flex; align-items: flex-start; flex-direction: column; + position: relative; ` -export const Tab = styled.div<{ selected?: boolean }>` +export const TabOptions = styled.div` display: flex; - padding: 12px; - border-bottom: ${({ selected }) => - selected ? '4px solid #9330FF' : '4px solid #fff'}; + align-items: center; ` -export const TabOptions = styled.div` +export const Tab = styled.button` display: flex; - align-items: center; + padding: 8px 12px; + box-shadow: none; + border-width: 0px; + border-bottom: 4px solid transparent; + background: transparent; + box-sizing: border-box; + margin: 0px; + position: relative; + cursor: pointer; ` -export const TabsBorder = styled.div` +export const TabSelectedBorder = styled(animated.div)` + position: absolute; height: 4px; - background: red; - width: 100%; + border-radius: 2px; + background: ${({ theme }) => theme.accent}; + bottom: 0; ` -export const TabBorderSelection = styled.div` - height: 100%; - background: blue; - width: 50px; +export const FontWeightCompensation = styled.div` + visibility: hidden; + + & > p { + margin: 0; + } +` + +export const TabContent = styled.div<{ selected?: boolean }>` + position: absolute; + top: 12px; + left: 12px; + + & > p { + margin: 0; + } ` diff --git a/applications/launchpad_v2/src/components/Tabs/types.ts b/applications/launchpad_v2/src/components/Tabs/types.ts index 6573b93969d..419cd485586 100644 --- a/applications/launchpad_v2/src/components/Tabs/types.ts +++ b/applications/launchpad_v2/src/components/Tabs/types.ts @@ -1,10 +1,12 @@ import { ReactNode } from 'react' +export interface TabProp { + id: string + content: ReactNode +} + export interface TabsProps { - tabs: { - id: string - content: ReactNode - }[] + tabs: TabProp[] selected: string onSelect: (id: string) => void } diff --git a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/components/DashboardTabs/index.tsx b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/components/DashboardTabs/index.tsx new file mode 100644 index 00000000000..8d1b0026128 --- /dev/null +++ b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/components/DashboardTabs/index.tsx @@ -0,0 +1,47 @@ +import { useDispatch, useSelector } from 'react-redux' +import Tabs from '../../../../../components/Tabs' + +import { setPage } from '../../../../../store/app' +import { ViewType } from '../../../../../store/app/types' +import { selectView } from '../../../../../store/app/selectors' +import { useEffect, useState } from 'react' + +/** + * Renders Dasboard tabs + */ +const DashboardTabs = () => { + const dispatch = useDispatch() + + const currentPage = useSelector(selectView) + + const [tabs, setTabs] = useState([ + { + id: 'MINING', + content: Mining, + }, + { + id: 'BASE_NODE', + content: Base Node, + }, + { + id: 'WALLET', + content: Wallet, + }, + ]) + + // useEffect(() => {}, []) + + const setPageTab = (tabId: string) => { + dispatch(setPage(tabId as ViewType)) + } + + return ( + + ) +} + +export default DashboardTabs diff --git a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx index 50048a86df2..cdbc42cbfe6 100644 --- a/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx +++ b/applications/launchpad_v2/src/containers/Dashboard/DashboardContainer/index.tsx @@ -1,4 +1,4 @@ -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { DashboardContent, DashboardLayout } from './styles' @@ -6,11 +6,9 @@ import MiningContainer from '../../MiningContainer' import BaseNodeContainer from '../../BaseNodeContainer' import WalletContainer from '../../WalletContainer' +import DashboardTabs from './components/DashboardTabs' import Footer from '../../../components/Footer' -import Tabs from '../../../components/Tabs' -import { setPage } from '../../../store/app' -import { ViewType } from '../../../store/app/types' import { selectView } from '../../../store/app/selectors' /** @@ -21,29 +19,8 @@ import { selectView } from '../../../store/app/selectors' * Dashboard view containing three main tabs: Mining, Wallet and BaseNode */ const DashboardContainer = () => { - const dispatch = useDispatch() - const currentPage = useSelector(selectView) - const pageTabs = [ - { - id: 'MINING', - content: Mining, - }, - { - id: 'BASE_NODE', - content: Base Node, - }, - { - id: 'WALLET', - content: Wallet, - }, - ] - - const setPageTab = (tabId: string) => { - dispatch(setPage(tabId as ViewType)) - } - const renderPage = () => { switch (currentPage) { case 'MINING': @@ -60,11 +37,7 @@ const DashboardContainer = () => { return ( - + {renderPage()}