⚡️ feat: create tabs component
This commit is contained in:
parent
b5eef95d15
commit
d0263f547e
53
packages/frontend/src/components/shared/Tabs/Tabs.theme.ts
Normal file
53
packages/frontend/src/components/shared/Tabs/Tabs.theme.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export type TabsVariants = VariantProps<typeof tabsTheme>;
|
||||||
|
|
||||||
|
export const tabsTheme = tv({
|
||||||
|
slots: {
|
||||||
|
root: ['flex', 'flex-col', 'w-full'],
|
||||||
|
triggerWrapper: [
|
||||||
|
'px-1',
|
||||||
|
'pb-5',
|
||||||
|
'text-elements-low-em',
|
||||||
|
'border-b-2',
|
||||||
|
'border-transparent',
|
||||||
|
'hover:border-border-interactive/10',
|
||||||
|
'hover:text-elements-mid-em',
|
||||||
|
'focus-within:border-border-interactive/10',
|
||||||
|
'data-[state=active]:font-medium',
|
||||||
|
'data-[state=active]:text-elements-high-em',
|
||||||
|
'data-[state=active]:border-elements-high-em',
|
||||||
|
],
|
||||||
|
trigger: [
|
||||||
|
'flex',
|
||||||
|
'gap-1.5',
|
||||||
|
'cursor-default',
|
||||||
|
'select-none',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
'outline-none',
|
||||||
|
'leading-none',
|
||||||
|
'tracking-[-0.006em]',
|
||||||
|
'rounded-md',
|
||||||
|
'focus-ring',
|
||||||
|
],
|
||||||
|
triggerList: [
|
||||||
|
'flex',
|
||||||
|
'shrink-0',
|
||||||
|
'gap-5',
|
||||||
|
'border-b',
|
||||||
|
'border-border-interactive/10',
|
||||||
|
],
|
||||||
|
content: ['text-elements-high-em', 'grow', 'outline-none', 'tab-content'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
fillWidth: {
|
||||||
|
true: {
|
||||||
|
trigger: ['flex-1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
fillWidth: false,
|
||||||
|
},
|
||||||
|
});
|
42
packages/frontend/src/components/shared/Tabs/Tabs.tsx
Normal file
42
packages/frontend/src/components/shared/Tabs/Tabs.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { type ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { Root as TabsRoot } from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { tabsTheme } from './Tabs.theme';
|
||||||
|
import TabsContent from './TabsContent';
|
||||||
|
import TabsList from './TabsList';
|
||||||
|
import TabsTrigger from './TabsTrigger';
|
||||||
|
import TabsProvider, { TabsProviderProps } from './TabsProvider';
|
||||||
|
|
||||||
|
export interface TabsProps extends ComponentPropsWithoutRef<typeof TabsRoot> {
|
||||||
|
/**
|
||||||
|
* The configuration for the tabs component.
|
||||||
|
*/
|
||||||
|
config?: TabsProviderProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that allows users to switch between different tabs.
|
||||||
|
* @returns JSX element representing the tabs component.
|
||||||
|
*/
|
||||||
|
export const Tabs = ({ config, className, ...props }: TabsProps) => {
|
||||||
|
const { root } = tabsTheme(config);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsProvider {...config}>
|
||||||
|
<TabsRoot {...props} className={root({ className })} />
|
||||||
|
</TabsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the TabsTrigger class to the Trigger property of the Tabs object.
|
||||||
|
*/
|
||||||
|
Tabs.Trigger = TabsTrigger;
|
||||||
|
/**
|
||||||
|
* Assigns the TabsList object to the List property of the Tabs object.
|
||||||
|
*/
|
||||||
|
Tabs.List = TabsList;
|
||||||
|
/**
|
||||||
|
* Assigns the TabsContent component to the Content property of the Tabs component.
|
||||||
|
*/
|
||||||
|
Tabs.Content = TabsContent;
|
@ -0,0 +1,26 @@
|
|||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
type ComponentPropsWithoutRef,
|
||||||
|
type ElementRef,
|
||||||
|
} from 'react';
|
||||||
|
import { Content } from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { tabsTheme } from '../Tabs.theme';
|
||||||
|
|
||||||
|
export interface TabsContentProps
|
||||||
|
extends ComponentPropsWithoutRef<typeof Content> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that represents the content of the tabs component.
|
||||||
|
*/
|
||||||
|
const TabsContent = forwardRef<ElementRef<typeof Content>, TabsContentProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const { content } = tabsTheme();
|
||||||
|
return <Content ref={ref} className={content({ className })} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assigns the display name to the TabsContent component.
|
||||||
|
TabsContent.displayName = 'TabsContent';
|
||||||
|
|
||||||
|
export { TabsContent };
|
@ -0,0 +1,3 @@
|
|||||||
|
import { TabsContent } from './TabsContent';
|
||||||
|
|
||||||
|
export default TabsContent;
|
@ -0,0 +1,25 @@
|
|||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
type ComponentPropsWithoutRef,
|
||||||
|
type ElementRef,
|
||||||
|
} from 'react';
|
||||||
|
import { List } from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { tabsTheme } from 'components/shared/Tabs/Tabs.theme';
|
||||||
|
|
||||||
|
export interface TabsListProps extends ComponentPropsWithoutRef<typeof List> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that represents the list of tabs.
|
||||||
|
*/
|
||||||
|
const TabsList = forwardRef<ElementRef<typeof List>, TabsListProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
const { triggerList } = tabsTheme({ className });
|
||||||
|
return <List ref={ref} className={triggerList({ className })} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assigns the display name to the TabsList component.
|
||||||
|
TabsList.displayName = 'TabsList';
|
||||||
|
|
||||||
|
export { TabsList };
|
@ -0,0 +1,3 @@
|
|||||||
|
import { TabsList } from './TabsList';
|
||||||
|
|
||||||
|
export default TabsList;
|
@ -0,0 +1,43 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
type PropsWithChildren,
|
||||||
|
} from 'react';
|
||||||
|
import { TabsVariants } from './Tabs.theme';
|
||||||
|
|
||||||
|
export interface TabsProviderProps extends Partial<TabsVariants> {}
|
||||||
|
|
||||||
|
type TabsProviderContext = ReturnType<typeof useTabsValues>;
|
||||||
|
|
||||||
|
const TabsContext = createContext<Partial<TabsProviderContext>>({});
|
||||||
|
|
||||||
|
// For inferring return type
|
||||||
|
const useTabsValues = (props: TabsProviderProps) => {
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A provider component that allows users to switch between different tabs.
|
||||||
|
* @returns JSX element representing the tabs provider component.
|
||||||
|
*/
|
||||||
|
export const TabsProvider = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<TabsProviderProps>): JSX.Element => {
|
||||||
|
const values = useTabsValues(props);
|
||||||
|
return <TabsContext.Provider value={values}>{children}</TabsContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns the context of the tabs provider.
|
||||||
|
* @returns The context of the tabs provider.
|
||||||
|
*/
|
||||||
|
export const useTabs = () => {
|
||||||
|
const context = useContext(TabsContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useTabs was used outside of its Provider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabsProvider;
|
@ -0,0 +1,49 @@
|
|||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
type ComponentPropsWithoutRef,
|
||||||
|
type ElementRef,
|
||||||
|
type PropsWithChildren,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import { Trigger } from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { tabsTheme } from 'components/shared/Tabs/Tabs.theme';
|
||||||
|
import { useTabs } from 'components/shared/Tabs/TabsProvider';
|
||||||
|
|
||||||
|
export interface TabsTriggerProps
|
||||||
|
extends ComponentPropsWithoutRef<typeof Trigger> {
|
||||||
|
/**
|
||||||
|
* The icon to display in the trigger.
|
||||||
|
*/
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that represents the trigger for the tabs component.
|
||||||
|
*/
|
||||||
|
const TabsTrigger = forwardRef<
|
||||||
|
ElementRef<typeof Trigger>,
|
||||||
|
PropsWithChildren<TabsTriggerProps>
|
||||||
|
>(({ className, icon, children, ...props }, ref) => {
|
||||||
|
const config = useTabs();
|
||||||
|
const { triggerWrapper, trigger } = tabsTheme(config);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Trigger
|
||||||
|
ref={ref}
|
||||||
|
tabIndex={-1}
|
||||||
|
className={triggerWrapper({ className })}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{/* Need to add button in the trigger children because there's focus state inside the children */}
|
||||||
|
<button className={trigger()}>
|
||||||
|
{children}
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
</Trigger>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TabsTrigger.displayName = 'TabsTrigger';
|
||||||
|
|
||||||
|
export { TabsTrigger };
|
@ -0,0 +1,3 @@
|
|||||||
|
import { TabsTrigger } from './TabsTrigger';
|
||||||
|
|
||||||
|
export default TabsTrigger;
|
1
packages/frontend/src/components/shared/Tabs/index.ts
Normal file
1
packages/frontend/src/components/shared/Tabs/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Tabs';
|
Loading…
Reference in New Issue
Block a user