Skip to content
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
3 changes: 2 additions & 1 deletion src/common/components/Dropdown/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createContext, PropsWithChildren, useContext, useState } from 'react';
import noop from 'lodash/noop';

import { BaseComponentProps } from 'common/utils/types';
import { cn } from 'common/utils/css';
Expand All @@ -18,7 +19,7 @@ type DropdownMenuContextValue = {
*/
const DropdownMenuContext = createContext<DropdownMenuContextValue>({
isHidden: true,
setIsHidden: () => {},
setIsHidden: noop,
});

/**
Expand Down
55 changes: 0 additions & 55 deletions src/common/components/Tabs/Tab.tsx

This file was deleted.

53 changes: 0 additions & 53 deletions src/common/components/Tabs/TabContent.tsx

This file was deleted.

47 changes: 0 additions & 47 deletions src/common/components/Tabs/TabList.tsx

This file was deleted.

191 changes: 162 additions & 29 deletions src/common/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { PropsWithChildren, useState } from 'react';
import { createContext, PropsWithChildren, useContext, useState } from 'react';
import { cva, VariantProps } from 'class-variance-authority';
import noop from 'lodash/noop';

import { cn } from 'common/utils/css';
import { BaseComponentProps } from 'common/utils/types';
import { TabsContext, TabsContextValue } from 'common/providers/TabsContext';
import Button from '../Button/Button';

/**
* Properties for the `TabsProvider` component.
* Defines the shape of the `TabsContext` value.
*/
interface TabsProviderProps extends PropsWithChildren, Pick<TabsContextValue, 'value'> {}
type TabsContextValue = {
activeTab: string;
setActiveTab: (tab: string) => void;
};

/**
* The `TabsProvider` component renders a React Context Provider which provides access
* to the the value of the `TabsContext`, i.e. the `TabsContextValue`.
* The `TabsContext` instance.
*/
const TabsProvider = ({ children, value }: TabsProviderProps): JSX.Element => {
const [selectedTab, setSelectedTab] = useState<string>(value);

const contextValue: TabsContextValue = {
setValue: setSelectedTab,
value: selectedTab,
};
return <TabsContext.Provider value={contextValue}>{children}</TabsContext.Provider>;
};
const TabsContext = createContext<TabsContextValue>({
activeTab: '',
setActiveTab: noop,
});

/**
* Properties for the `Tabs` component.
Expand All @@ -32,35 +31,169 @@ export interface TabsProps extends BaseComponentProps, PropsWithChildren {
}

/**
* The `Tabs` component is a wrapper for rendering tabbed content. Compose tabbed
* content using: `Tabs`, `TabList`, `Tab`, and `TabContent` as illustrated in
* The `Tabs` component is used to display tabbed content. Compose tabbed
* content using: `List`, `Tab`, and `Content` as illustrated in
* the example.
*
* Note: The `defaultValue` property must match the `value` of one of the Tab
* components.
*
* *Example:*
* ```
* <Tabs defaultValue="list" className="w-full">
* <TabList>
* <Tab value="list">List</Tab>
* <Tab value="detail">Detail</Tab>
* </TabList>
* <TabContent value="list" className="py-8">
* <div className="font-bold">I am the LIST tab.</div>
* </TabContent>
* <TabContent value="detail" className="py-8">
* <div className="font-bold">I am the DETAIL tab.</div>
* </TabContent>
* <Tabs defaultValue="list">
* <Tabs.List>
* <Tabs.Tab value="list">List</Tab>
* <Tabs.Tab value="detail">Detail</Tab>
* </Tabs.List>
* <Tabs.Content value="list">
* <div className="font-bold py-8">I am the LIST tab.</div>
* </Tabs.Content>
* <Tabs.Content value="detail">
* <div className="font-bold py-8">I am the DETAIL tab.</div>
* </Tabs.Content>
* </Tabs>
* ```
*/
const Tabs = ({ children, className, defaultValue, testId = 'tabs' }: TabsProps): JSX.Element => {
const [activeTab, setActiveTab] = useState(defaultValue);

return (
<div className={cn(className)} data-testid={testId}>
<TabsProvider value={defaultValue}>{children}</TabsProvider>
<TabsContext.Provider value={{ activeTab, setActiveTab }}>{children}</TabsContext.Provider>
</div>
);
};

/**
* Defines the `List` component base and variant styles.
*/
const listVariants = cva('flex gap-4 border-b border-b-neutral-500/10', {
variants: {
align: {
start: 'justify-start',
stretch: '*:grow',
},
},
defaultVariants: { align: 'start' },
});

/**
* Properties for the `List` component.
*/
interface ListProps
extends BaseComponentProps,
PropsWithChildren,
VariantProps<typeof listVariants> {}

/**
* The `List` component is a container for one to many `Tab` components. It
* renders a horizontal bar of clickable tabs.
*/
const List = ({
align = 'start',
children,
className,
testId = 'tabs-list',
}: ListProps): JSX.Element => {
return (
<div className={cn(listVariants({ align, className }))} data-testid={testId}>
{children}
</div>
);
};
Tabs.List = List;

/**
* Defines the `Tab` component base and variant styles.
*/
const tabVariants = cva('rounded-none border-0 border-b-2 px-4 text-sm font-bold uppercase', {
variants: {
active: {
false: 'border-transparent',
true: 'border-blue-300 dark:border-blue-600',
},
},
defaultVariants: {
active: false,
},
});

/**
* Properties for the `Tab` component.
*/
interface TabProps extends BaseComponentProps, PropsWithChildren {
value: string;
}

/**
* The `Tab` component renders a clickable tab button which, when clicked,
* displays the associated tab content.
*
* Note: The `Tab` and its associated `TabContent` must have the same `value`
* property value.
*/
const Tab = ({ children, className, testId = 'tabs-tab', value }: TabProps): JSX.Element => {
const { activeTab, setActiveTab } = useContext(TabsContext);

const active = value === activeTab;

return (
<Button
variant="text"
className={cn(tabVariants({ active, className }))}
onClick={() => setActiveTab(value)}
testId={testId}
>
{children}
</Button>
);
};
Tabs.Tab = Tab;

/**
* Defines the `Content` component base and variant styles.
*/
const contentVariants = cva('', {
variants: {
active: {
true: 'block',
false: 'hidden',
},
},
defaultVariants: {
active: false,
},
});

/**
* Properties for the `Content` component.
*/
interface TabContentProps extends BaseComponentProps, PropsWithChildren {
value: string;
}

/**
* The `TabContent` component renders the content for a single tab.
*
* Note: The `TabContent` and its associated `Tab` must have the same `value`
* property value.
*/
const Content = ({
children,
className,
testId = 'tabs-content',
value,
}: TabContentProps): JSX.Element => {
const { activeTab } = useContext(TabsContext);

const active = value === activeTab;

return (
<div className={cn(contentVariants({ active, className }))} data-testid={testId}>
{children}
</div>
);
};
Tabs.Content = Content;

export default Tabs;
Loading