Skip to content
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

feat: tabs and ts issue #94

Merged
merged 3 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }>`
Copy link

@corquaid corquaid Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is selected prop being used anywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right! I'm going to merge this PR and add new one fixing this.

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