From c4c12b4c5a77602820b760382d6bfdc21f7a5518 Mon Sep 17 00:00:00 2001 From: Alina Date: Fri, 22 Mar 2024 18:36:06 +0400 Subject: [PATCH] chore: add tabs component --- src/shared/ui/Button/styles.module.scss | 4 +- src/shared/ui/Tabs/helpers/a11yProps.ts | 5 ++ src/shared/ui/Tabs/helpers/index.ts | 1 + src/shared/ui/Tabs/index.ts | 2 + src/shared/ui/Tabs/model/types.ts | 1 + src/shared/ui/Tabs/ui/Tab.tsx | 45 ++++++++++++++ src/shared/ui/Tabs/ui/TabPanel.tsx | 42 +++++++++++++ src/shared/ui/Tabs/ui/Tabs.tsx | 36 +++++++++++ src/shared/ui/Tabs/ui/TabsList.tsx | 77 ++++++++++++++++++++++++ src/shared/ui/Tabs/ui/index.ts | 4 ++ src/shared/ui/Tabs/ui/styles.module.scss | 53 ++++++++++++++++ src/shared/ui/icons/ArrowUp.tsx | 8 +++ 12 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/shared/ui/Tabs/helpers/a11yProps.ts create mode 100644 src/shared/ui/Tabs/helpers/index.ts create mode 100644 src/shared/ui/Tabs/index.ts create mode 100644 src/shared/ui/Tabs/model/types.ts create mode 100644 src/shared/ui/Tabs/ui/Tab.tsx create mode 100644 src/shared/ui/Tabs/ui/TabPanel.tsx create mode 100644 src/shared/ui/Tabs/ui/Tabs.tsx create mode 100644 src/shared/ui/Tabs/ui/TabsList.tsx create mode 100644 src/shared/ui/Tabs/ui/index.ts create mode 100644 src/shared/ui/Tabs/ui/styles.module.scss create mode 100644 src/shared/ui/icons/ArrowUp.tsx diff --git a/src/shared/ui/Button/styles.module.scss b/src/shared/ui/Button/styles.module.scss index f2633a3..bd95a3e 100644 --- a/src/shared/ui/Button/styles.module.scss +++ b/src/shared/ui/Button/styles.module.scss @@ -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; diff --git a/src/shared/ui/Tabs/helpers/a11yProps.ts b/src/shared/ui/Tabs/helpers/a11yProps.ts new file mode 100644 index 0000000..6b4acc9 --- /dev/null +++ b/src/shared/ui/Tabs/helpers/a11yProps.ts @@ -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}`, +}) diff --git a/src/shared/ui/Tabs/helpers/index.ts b/src/shared/ui/Tabs/helpers/index.ts new file mode 100644 index 0000000..32271e2 --- /dev/null +++ b/src/shared/ui/Tabs/helpers/index.ts @@ -0,0 +1 @@ +export { a11yProps } from './a11yProps' diff --git a/src/shared/ui/Tabs/index.ts b/src/shared/ui/Tabs/index.ts new file mode 100644 index 0000000..d24845c --- /dev/null +++ b/src/shared/ui/Tabs/index.ts @@ -0,0 +1,2 @@ +export * from './ui' +export * from './helpers' diff --git a/src/shared/ui/Tabs/model/types.ts b/src/shared/ui/Tabs/model/types.ts new file mode 100644 index 0000000..d4ed1b2 --- /dev/null +++ b/src/shared/ui/Tabs/model/types.ts @@ -0,0 +1 @@ +export type TTabValue = string | null diff --git a/src/shared/ui/Tabs/ui/Tab.tsx b/src/shared/ui/Tabs/ui/Tab.tsx new file mode 100644 index 0000000..cc99d03 --- /dev/null +++ b/src/shared/ui/Tabs/ui/Tab.tsx @@ -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 ( + + ) +} diff --git a/src/shared/ui/Tabs/ui/TabPanel.tsx b/src/shared/ui/Tabs/ui/TabPanel.tsx new file mode 100644 index 0000000..7a172db --- /dev/null +++ b/src/shared/ui/Tabs/ui/TabPanel.tsx @@ -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 ( +
+ {children} +
+ ) +} diff --git a/src/shared/ui/Tabs/ui/Tabs.tsx b/src/shared/ui/Tabs/ui/Tabs.tsx new file mode 100644 index 0000000..0e79610 --- /dev/null +++ b/src/shared/ui/Tabs/ui/Tabs.tsx @@ -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(defaultValue ?? null) + + return ( + + {children} + + ) +} + +export const useTabContext = () => useContext(TabsContext) diff --git a/src/shared/ui/Tabs/ui/TabsList.tsx b/src/shared/ui/Tabs/ui/TabsList.tsx new file mode 100644 index 0000000..2ac7532 --- /dev/null +++ b/src/shared/ui/Tabs/ui/TabsList.tsx @@ -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(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 ( +
+ {arrows && ( +
+ ) +} diff --git a/src/shared/ui/Tabs/ui/index.ts b/src/shared/ui/Tabs/ui/index.ts new file mode 100644 index 0000000..e6eb699 --- /dev/null +++ b/src/shared/ui/Tabs/ui/index.ts @@ -0,0 +1,4 @@ +export { Tabs } from './Tabs' +export { TabsList } from './TabsList' +export { TabPanel } from './TabPanel' +export { Tab } from './Tab' diff --git a/src/shared/ui/Tabs/ui/styles.module.scss b/src/shared/ui/Tabs/ui/styles.module.scss new file mode 100644 index 0000000..754b4f7 --- /dev/null +++ b/src/shared/ui/Tabs/ui/styles.module.scss @@ -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); + } +} diff --git a/src/shared/ui/icons/ArrowUp.tsx b/src/shared/ui/icons/ArrowUp.tsx new file mode 100644 index 0000000..d74ce51 --- /dev/null +++ b/src/shared/ui/icons/ArrowUp.tsx @@ -0,0 +1,8 @@ +export const ArrowUp = () => ( + +)