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

Shayan/webrel 2149/implement vertical tab #65

Merged
merged 13 commits into from
Feb 9, 2024
Merged
59 changes: 59 additions & 0 deletions lib/components/VerticalTab/CollapsibleVerticalTabItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

import React, { useState } from 'react';
import { Text } from '../Text';
import clsx from 'clsx';
import { VerticalTabItem, TabItem } from './VerticalTabItem';

type CollapsibleVerticalTabItemProps = {
item: TabItem;
onSelectItemHandler: (title: string) => void;
selectedTab: string;
className?: string;
iconClassName?: string;
}


const ArrowIcon = ({ is_open }: { is_open: boolean }) => <svg className={clsx(`vertical-tab__arrow`, {
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
'vertical-tab__arrow--open': is_open,
})} xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
<path d="M8.66699 9.58579L13.9599 4.29289C14.3504 3.90237 14.9836 3.90237 15.3741 4.29289C15.7646 4.68342 15.7646 5.31658 15.3741 5.70711L9.3741 11.7071C8.98358 12.0976 8.35041 12.0976 7.95989 11.7071L1.95989 5.70711C1.56936 5.31658 1.56936 4.68342 1.95989 4.29289C2.35041 3.90237 2.98357 3.90237 3.3741 4.29289L8.66699 9.58579Z" fill="#333333" />
</svg>;
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved

export const CollapsibleVerticalTabItem = ({
item,
onSelectItemHandler,
selectedTab,
className,
iconClassName,
}: CollapsibleVerticalTabItemProps) => {
const selectedSubItemSelected = item?.subItems?.find((subItem) => subItem?.title === selectedTab)
const [open, setOpen] = useState(selectedSubItemSelected ? true : false);

const onClickHandler = () => {
const shouldCollapse = !selectedSubItemSelected;
if (shouldCollapse)
setOpen(!open);
}
return (
<div
key={item.title}
className={clsx(`collapsible-vertical-tab`)}>
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
<div className={clsx(`collapsible-vertical-tab__header`, {
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
'collapsible-vertical-tab__header--open': selectedSubItemSelected,
})}
onClick={() => onClickHandler()}
>
<span className={clsx(`vertical-tab__icon`, iconClassName)}> {item?.icon}</span>
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
<Text className='vertical-tab__label'>{item?.title}</Text>
<ArrowIcon is_open={open} />
</div>
{open && <div className='collapsible-vertical-tab__items'>
{item?.subItems?.map((subItem) => {
return (
<VerticalTabItem className={className} selectedTab={selectedTab} key={subItem?.title} tab={subItem} onClick={() => onSelectItemHandler(subItem?.title)} />
)
})}
</div>}
</div>
)
}
107 changes: 107 additions & 0 deletions lib/components/VerticalTab/VerticalTab.scss
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
.vertical-tab__wrapper {
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: any reason for not putting this inside .vertical-tab ?

padding: 8px;
display: flex;
justify-content: center;
gap:25px;
@include mobile {
flex-direction: column;;
}
}

.vertical-tab {
&__items-container{
background: #f2f3f4;
flex: 1;
border-radius: 4px;
padding: 8px;
}

&__item {
display: flex;
padding: 10px 16px;
align-items: center;
cursor: pointer;
border-radius: 4px;

&:hover{
background-color: #e6e9e9;
}

&--active {
background-color: #fff;
border-left: 4px solid #ff444f;
padding-left: 12px;

&:hover{
background-color: #fff;
}
}

&--disabled {
& > span{
color: #999999;
}

&:hover{
cursor: unset;
background-color: inherit;
}
}
}

&__icon {
width: 16px;
height: 16px;
margin-right: 16px;
}

&__label {
flex:1;
}

&__pane {
flex: 2;
}

&__arrow--open{
transform: rotate(180deg);
}
}

.collapsible-vertical-tab {
Copy link
Contributor

Choose a reason for hiding this comment

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

that should be in separate file for separate component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but in this case does it make any difference? because the consumer should use them all together. so that's why I put all styles in one file to avoid external request from consumer side

&__header {
display: flex;
padding: 10px 16px;
align-items: center;
cursor: pointer;
border-radius: 4px;

&--open > :nth-child(2) {
font-weight: bold;
}
}

&__icon {
width: 16px;
height: 16px;
margin-right: 16px;
}

&__label {
flex:1;
}
}


.test-1{
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
background-color: purple;
}
.test-2{
background-color: forestgreen;
}
.test-3{
background-color: hotpink;
}
.test-item{
background-color: yellow;
}
18 changes: 18 additions & 0 deletions lib/components/VerticalTab/VerticalTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { memo } from 'react';
import clsx from 'clsx';
import './VerticalTab.scss';

type VerticalTabProps = {
className?: string;
}

export const VerticalTab = memo(({
className,
children
}: React.PropsWithChildren<VerticalTabProps>) => {
return (
<div className={clsx(`vertical-tab__wrapper`, className)}>
{children}
</div>
);
})
40 changes: 40 additions & 0 deletions lib/components/VerticalTab/VerticalTabItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import clsx from 'clsx';

import { Text } from '../Text';

export type TabItem = {
icon?: React.ReactNode;
is_disabled?: boolean;
panel?: React.ReactNode;
subItems?: {
is_disabled?: boolean;
panel: React.ReactNode;
title: string;
}[];
title: string;
}

type VerticalTabItemProps = {
tab: TabItem;
onClick: (title: string) => void;
className?: string;
selectedTab: string;
}

export const VerticalTabItem = ({ tab, onClick, className, selectedTab }: VerticalTabItemProps) => {
return (
<div
className={
clsx(`vertical-tab__item`, {
'vertical-tab__item--active': tab.title === selectedTab,
Copy link
Contributor

Choose a reason for hiding this comment

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

is tab.title an arbitrary string, or is it a specific enum?
based on the naming itself, feels like one is title/translation and the other one is enum

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's string but not optional param.
I didn't understand the issue. could u pleas elaborate more?

'vertical-tab__item--disabled': tab.is_disabled
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm seems like the condition for --active and --disabled is different,
does not feel good, it feels like it migth be possible to have both --disabled and --enabled class or none,
ideallly, they both should use the same flag/variable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but they are totally different. with active we only handle the style. but with disabled we also disable the functionality for onClick as well/ that's why I used two different things.

}, className)
}
onClick={() => !tab.is_disabled && onClick(tab.title)}
>
<span className='vertical-tab__icon'> {tab?.icon}</span>
<Text as='span' className='vertical-tab__label'>{tab.title}</Text>
</div>
)
}
129 changes: 129 additions & 0 deletions lib/components/VerticalTab/VerticalTabItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { memo, useEffect, useState } from 'react';
import { CollapsibleVerticalTabItem } from './CollapsibleVerticalTabItem';
import { VerticalTabItem, type TabItem } from './VerticalTabItem'
import clsx from 'clsx';

type VerticalTabItemsProps = {
activeTab: string;
onSelectItem?: (title: string) => void;
wrapperClassName?: string;
panelClassName?: string;
itemClassName?: string;
items: TabItem[];
}

/**
* Component to display the vertical tab items. iit should be wrapperd inside the VerticalTab component
* @param {TabItem} items - tab items
* @param {string} panelClassName -it applies the classname to the right panel
* @param {string} wrapperClassName - it applies the classname to the left side menu container
* @param {string} itemClassName - it applies the classname to the each items whether it's sub-item or single item
* @param {string} activeTab - indicates the active tab. you can pass the title of the tab
* @param {Function} onSelectItem - callback to handle selecting each tab item
* @returns {React.JSX.Element} - returns the vertical tab component
*
* @example
* const items = [
* {
* title: 'Item 1',
* icon: Icon,
* panel: <div>Item 1 pane</div>
* },
* {
* title: 'Item 2',
* icon: Icon,
* panel: <div>Item 2 pane</div>,
* subItems: [
* {
* title: 'Item 2.1',
* icon: Icon,
* panel: <div>Item 2.1 pane</div>
* },
* {
* title: 'Item 2.2',
* icon: Icon,
* is_disabled: true,
* panel: <div>Item 2.2 pane</div>
* },
* ]
* },
* {
* title: 'Item 3',
* icon: Icon,
* panel: <div>Item 3 pane</div>
* },
* ]
*
* <VerticalTab className='test-1'>
* <VerticalTabItems
* items={items} activeTab='SubItem 2.1'
* onSelectItem={
* (title) => console.log('clicked on:', title)
* }
* />
* </VerticalTab>
*/

export const VerticalTabItems = memo(({
items,
panelClassName,
wrapperClassName,
itemClassName,
shayan-deriv marked this conversation as resolved.
Show resolved Hide resolved
activeTab,
onSelectItem }: VerticalTabItemsProps) => {
const [selectedTab, setSelectedTab] = useState<string>(activeTab);
useEffect(() => {
if (activeTab) {
setSelectedTab(activeTab);
}
}, [activeTab]);

const findActiveTab = (title: string) => {
for (const item of items) {
if (item?.subItems) {
const foundItem = item?.subItems.find((subItem) => subItem?.title === title);
if (foundItem) {
return foundItem;
}
} else {
if (item?.title === title) {
return item;
}
}
}
}

const onSelectItemHandler = (title: string) => {
const new_active_tab = findActiveTab(title)?.title;
setSelectedTab(() => new_active_tab ?? activeTab);
onSelectItem?.(title);
}


return (
<>
<div className={clsx('vertical-tab__items-container', wrapperClassName)}>
{items.map((item) => {
if (!item?.subItems) {
return (
<VerticalTabItem className={itemClassName} key={item?.title} selectedTab={selectedTab} tab={item} onClick={() => onSelectItemHandler(item?.title)} />
)
} else {
return (
<CollapsibleVerticalTabItem
key={item?.title}
item={item}
selectedTab={selectedTab}
onSelectItemHandler={onSelectItemHandler}
className={itemClassName}
/>
)
}
})}
</div>
<div className={clsx('vertical-tab__pane', panelClassName)}>
{findActiveTab(selectedTab)?.panel}
</div>
</>
);
})
2 changes: 2 additions & 0 deletions lib/components/VerticalTab/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {VerticalTab} from './VerticalTab'
export {VerticalTabItems} from './VerticalTabItems'
1 change: 1 addition & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export { useOnClickOutside } from "./hooks/useOnClickOutside";
export { PasswordInput } from "./components/PasswordInput";
export { InlineMessage } from "./components/InlineMessage";
export { Checkbox } from "./components/Checkbox";
export { VerticalTab, VerticalTabItems } from "./components/VerticalTab";
Loading