Skip to content

Commit

Permalink
Merge pull request #9 from Rue-pro/ui-tabs-component
Browse files Browse the repository at this point in the history
chore: add tabs component
  • Loading branch information
Rue-pro authored Mar 22, 2024
2 parents abdade9 + c4c12b4 commit 1bedb56
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/shared/ui/Button/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
user-select: none;
border: 1px solid;
border-radius: var(--radius-1);
transition: border-color 0.25s;
transition:
border-color 0.25s,
outline 0.25s;

&:disabled {
cursor: not-allowed;
Expand Down
5 changes: 5 additions & 0 deletions src/shared/ui/Tabs/helpers/a11yProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const a11yProps = (tabName: string, index: number | string) => ({
id: `${tabName}-tab-${index}`,
'aria-controls': `${tabName}-tabpanel-${index}`,
'aria-labelledby': `${tabName}-tab-${index}`,
})
1 change: 1 addition & 0 deletions src/shared/ui/Tabs/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { a11yProps } from './a11yProps'
2 changes: 2 additions & 0 deletions src/shared/ui/Tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ui'
export * from './helpers'
1 change: 1 addition & 0 deletions src/shared/ui/Tabs/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type TTabValue = string | null
45 changes: 45 additions & 0 deletions src/shared/ui/Tabs/ui/Tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import cn from 'classnames'
import { ComponentChildren } from 'preact'

import { TTabValue } from '../model/types'
import { useTabContext } from './Tabs'
import styles from './styles.module.scss'

interface Props {
className?: string
value: TTabValue
disabled?: boolean
children: ComponentChildren
id: string
'aria-controls': string
}

export const Tab = ({
className,
value,
disabled,
children,
id,
'aria-controls': ariaControls,
}: Props) => {
const { currentTab, selectTab } = useTabContext()
const selected = currentTab === value

const tabClass = cn('h1', styles.tab, className ?? '')

return (
<button
id={id}
className={tabClass}
tabIndex={selected ? 0 : -1}
type="button"
role="tab"
aria-selected={selected}
disabled={disabled}
onClick={() => selectTab(value)}
aria-controls={ariaControls}
>
{children}
</button>
)
}
42 changes: 42 additions & 0 deletions src/shared/ui/Tabs/ui/TabPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import cn from 'classnames'
import { ComponentChildren } from 'preact'

import { TTabValue } from '../model/types'
import { useTabContext } from './Tabs'
import styles from './styles.module.scss'

interface Props {
className?: string
children: ComponentChildren
value: TTabValue
'aria-labelledby': string
'aria-controls': string
}

export const TabPanel = ({
className,
children,
value,
'aria-labelledby': ariaLabelledBy,
'aria-controls': ariaControls,
}: Props) => {
const { currentTab } = useTabContext()
const selected = currentTab === value

const tabPanelClass = cn({
[styles.tabpanel]: true,
[styles['tabpanel--active']]: selected,
[className ?? '']: !!className,
})

return (
<div
className={tabPanelClass}
id={ariaControls}
role="tabpanel"
aria-labelledby={ariaLabelledBy}
>
{children}
</div>
)
}
36 changes: 36 additions & 0 deletions src/shared/ui/Tabs/ui/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ComponentChildren, createContext } from 'preact'
import { useContext, useState } from 'preact/hooks'

import { TTabValue } from '../model/types'

const TabsContext = createContext<{
currentTab: TTabValue
selectTab: (tab: TTabValue) => void
}>({
currentTab: null,
selectTab: () => {},
})

interface Props {
children: ComponentChildren
defaultValue?: TTabValue
value?: TTabValue
onChange?: (tab: TTabValue) => void
}

export const Tabs = ({ children, defaultValue, value, onChange }: Props) => {
const [currentTab, setCurrentTab] = useState<TTabValue>(defaultValue ?? null)

return (
<TabsContext.Provider
value={{
currentTab: value ? value : currentTab,
selectTab: value ? onChange ?? setCurrentTab : setCurrentTab,
}}
>
{children}
</TabsContext.Provider>
)
}

export const useTabContext = () => useContext(TabsContext)
77 changes: 77 additions & 0 deletions src/shared/ui/Tabs/ui/TabsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import cn from 'classnames'
import { ComponentChildren } from 'preact'
import { useRef } from 'preact/hooks'

import { Button } from '@shared/ui/Button'
import { ArrowUp } from '@shared/ui/icons/ArrowUp'

import styles from './styles.module.scss'

interface Props {
className?: string
children: ComponentChildren
arrows?: boolean
}

export const TabsList = ({ className, children, arrows }: Props) => {
const tabsRef = useRef<HTMLDivElement>(null)

const move = (direction: 'back' | 'forward') => () => {
const tab = tabsRef.current
if (!tab) return

const delta = 100

let movePosition = 0
if (direction === 'back') {
movePosition = tab.scrollLeft - delta

if (movePosition < 0) movePosition = 0
} else {
movePosition = tab.scrollLeft + delta

if (movePosition > tab.scrollWidth) movePosition = tab.scrollWidth
}

tab.scroll({ left: movePosition })
}

const tabListClass = cn({
[styles.tablist]: true,
[styles['tablist--with-arrows']]: arrows,
})

return (
<div className={tabListClass}>
{arrows && (
<Button
variant="secondary"
className={styles['tablist__button-left']}
onClick={move('back')}
startIcon={<ArrowUp />}
aria-label="Back"
/>
)}

<div
ref={tabsRef}
className={cn(styles.tablist__tabs, className ?? '')}
tabIndex={-1}
aria-orientation="vertical"
role="tablist"
>
{children}
</div>

{arrows && (
<Button
variant="secondary"
className={styles['tablist__button-right']}
onClick={move('forward')}
startIcon={<ArrowUp />}
aria-label="Forward"
/>
)}
</div>
)
}
4 changes: 4 additions & 0 deletions src/shared/ui/Tabs/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Tabs } from './Tabs'
export { TabsList } from './TabsList'
export { TabPanel } from './TabPanel'
export { Tab } from './Tab'
53 changes: 53 additions & 0 deletions src/shared/ui/Tabs/ui/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.tablist {
display: grid;
gap: var(--spacing-2);
max-width: 100%;

&--with-arrows {
grid-template-columns: auto 1fr auto;
}

&__tabs {
display: flex;
gap: var(--spacing-2);
overflow-x: auto;

&::-webkit-scrollbar {
display: none;
}
}

&__button-left {
transform: rotate(-90deg);
}

&__button-right {
transform: rotate(90deg);
}
}

.tabpanel {
display: none;
flex-direction: column;
gap: var(--spacing-2);
padding-top: var(--spacing-2);

&--active {
display: flex;
}
}

.tab {
cursor: pointer;
background-color: transparent;
border-inline: none;
border-top: none;
border-bottom: 2px solid transparent;

&[aria-selected='true'] {
background: var(--color-brand-700);
background-clip: text;
-webkit-text-fill-color: transparent;
border-bottom: 2px solid var(--color-brand-700);
}
}
8 changes: 8 additions & 0 deletions src/shared/ui/icons/ArrowUp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ArrowUp = () => (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m4 12 1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8z"
fill="currentColor"
></path>
</svg>
)

0 comments on commit 1bedb56

Please sign in to comment.