Skip to content

Commit

Permalink
feat: tabs and ts issue (#94)
Browse files Browse the repository at this point in the history
* Improve Tabs component and its usage in DashboardContainer

* Upgrade Tabs component and fix TS

* Fix text style type
  • Loading branch information
tomaszantas authored Apr 27, 2022
1 parent 36441a9 commit 01e5179
Show file tree
Hide file tree
Showing 30 changed files with 473 additions and 90 deletions.
7 changes: 6 additions & 1 deletion applications/launchpad_v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"@headlessui/react": "^1.5.0",
"@reduxjs/toolkit": "^1.8.1",
"@tauri-apps/api": "^1.0.0-rc.3",
"@types/jest": "^27.0.1",
"@types/jest": "^27.4.1",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
Expand All @@ -33,6 +33,11 @@
"dev": "tauri dev",
"bundle": "tauri build"
},
"fork-ts-checker": {
"typescript": {
"memoryLimit": 8192
}
},
"eslintConfig": {
"extends": [
"react-app",
Expand Down
2 changes: 2 additions & 0 deletions applications/launchpad_v2/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import themes from './styles/themes'

beforeAll(() => {
window.crypto = {
// @ts-expect-error: ignore this
getRandomValues: function (buffer) {
// @ts-expect-error: ignore this
return randomFillSync(buffer)
},
}
Expand Down
1 change: 1 addition & 0 deletions applications/launchpad_v2/src/components/Button/styles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable indent */
import styled from 'styled-components'

import { ButtonProps } from './types'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { tauriIPCMock } from '../../../__tests__/mocks/mockTauriIPC'

beforeAll(() => {
window.crypto = {
// @ts-expect-error: ignore this
getRandomValues: function (buffer) {
// @ts-expect-error: ignore this
return randomFillSync(buffer)
},
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ReactNode } from 'react'

import { IconsWrapper, KeyTile, LetterKey } from './styles'
import { KeyboardKeysProps } from './types'

import SvgCmdKey from '../../styles/Icons/CmdKey'
import SvgWinKey from '../../styles/Icons/WinKey'

Expand All @@ -15,7 +18,7 @@ import SvgWinKey from '../../styles/Icons/WinKey'
* <KeyboardKeys keys={['Ctrl', 'Alt', 'win']} />
*/
const KeyboardKeys = ({ keys }: KeyboardKeysProps) => {
const result = []
const result: ReactNode[] = []

keys.forEach((key, idx) => {
let symbol
Expand Down
12 changes: 10 additions & 2 deletions applications/launchpad_v2/src/components/Loading/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ import { StyledSpan } from './styles'
* @prop {string} [testId] - optional testId to assign for testing purposes
*/

const Loading = ({ loading, testId }: { loading?: boolean; testId?: string }) =>
const Loading = ({
loading,
size = '20px',
testId,
}: {
loading?: boolean
size?: string
testId?: string
}) =>
loading ? (
<StyledSpan data-testid={testId || 'loading-indicator'}>
<LoadingIcon />
<LoadingIcon width={size} height={size} />
</StyledSpan>
) : null

Expand Down
4 changes: 3 additions & 1 deletion applications/launchpad_v2/src/components/Loading/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ const spinKeyframes = keyframes`
`

export const StyledSpan = styled.span`
animation: ${spinKeyframes} infinite 2s linear;
& > svg {
animation: ${spinKeyframes} infinite 2s linear;
}
`
17 changes: 14 additions & 3 deletions applications/launchpad_v2/src/components/Select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
OptionsContainer,
Option,
} from './styles'
import { SelectProps } from './types'
import { Option as OptionProp } from './types'

/**
* @TODO go back to import SelectProps - it was switched, because eslint was giving some react/prop-types error
*/

/**
* @name Select
Expand All @@ -37,10 +41,17 @@ const Select = ({
inverted,
label,
disabled,
}: SelectProps) => {
}: {
disabled?: boolean
inverted?: boolean
label: string
value?: OptionProp
options: OptionProp[]
onChange: (option: OptionProp) => void
}) => {
return (
<StyledListbox value={value} onChange={onChange} disabled={disabled}>
{({ open }) => (
{({ open }: { open: boolean }) => (
<>
<Label inverted={inverted}>{label}</Label>
<SelectButton open={open} inverted={inverted} disabled={disabled}>
Expand Down
12 changes: 9 additions & 3 deletions applications/launchpad_v2/src/components/Select/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { ReactNode } from 'react'

export type SelectInternalProps = {
export interface SelectInternalProps {
disabled?: boolean
inverted?: boolean
children?: ReactNode
open?: boolean
}

type Option = { value: string; label: string; key: string }
export type SelectProps = {
export interface Option {
value: string
label: string
key: string
}

export interface SelectProps {
keys: string[]
disabled?: boolean
inverted?: boolean
label: string
Expand Down
34 changes: 34 additions & 0 deletions applications/launchpad_v2/src/components/Tabs/Tabs.test.tsx
Original file line number Diff line number Diff line change
@@ -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: <span>First tab</span>,
},
{
id: 'second-tab',
content: <span>Second tab</span>,
},
]

describe('Tabs', () => {
it('should render without crashing', async () => {
const selected = 'second-tab'
const onSelect = jest.fn()

await act(async () => {
render(
<ThemeProvider theme={themes.light}>
<Tabs tabs={tabs} selected={selected} onSelect={onSelect} />
</ThemeProvider>,
)
})

const firstTabText = screen.queryAllByText('First tab')
expect(firstTabText.length).toBeGreaterThan(0)
})
})
104 changes: 101 additions & 3 deletions applications/launchpad_v2/src/components/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,118 @@
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 renders the set of tab header tiles.
*
* @param {TabsProps} props - props of the Tabs component
*
* @typedef TabsProps
* @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)[]>([])

// The animation of the bottom 'border' that indicates the selected tab,
// is based on sizes of the rendered tabs. It means, that the componenets
// have to be rendered first, then the parent component can read widths,
// and finally the size and shift can ba calculated.
// Also, the Tabs component needs to re-render tabs twice on the initial mount,
// because the selected tab uses bold font, which changes tabs widths.
const [initialized, setInitialzed] = useState(0)

useEffect(() => {
tabsRefs.current = tabsRefs.current.slice(0, tabs.length)
setInitialzed(1)
}, [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 (
<TabsContainer>
<TabOptions>
{tabs.map((tab, index) => (
<Tab
key={`tab-${index}`}
selected={selected === tab.id}
ref={el => (tabsRefs.current[index] = el)}
onClick={() => onSelect(tab.id)}
>
{tab.content}
<FontWeightCompensation>
<Text
as={'span'}
type='defaultHeavy'
style={{ whiteSpace: 'nowrap', width: '100%' }}
>
{tab.content}
</Text>
</FontWeightCompensation>
<TabContent selected={selected === tab.id}>
<Text
as={'span'}
type={selected === tab.id ? 'defaultHeavy' : 'defaultMedium'}
style={{ whiteSpace: 'nowrap', width: '100%' }}
>
{tab.content}
</Text>
</TabContent>
</Tab>
))}
</TabOptions>
<TabSelectedBorder
style={{
...activeBorder,
}}
/>
</TabsContainer>
)
}
Expand Down
54 changes: 42 additions & 12 deletions applications/launchpad_v2/src/components/Tabs/styles.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
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;
white-space: no-wrap;
`

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;
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;
align-items: center;
`

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: 0;
left: 0;
display: flex;
padding: 12px;
width: 100%;
align-items: center;
justify-content: center;
box-sizing: border-box;
& > p {
margin: 0;
}
`
10 changes: 6 additions & 4 deletions applications/launchpad_v2/src/components/Tabs/types.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 01e5179

Please sign in to comment.