Tabs almost working

This commit is contained in:
Maxime Lalo 2024-07-17 15:52:34 +02:00
parent 478aebfeb3
commit 327ba170ef
8 changed files with 298 additions and 17 deletions

View File

@ -8,6 +8,7 @@ type IProps = {
color?: ETypoColor;
className?: string;
title?: string;
onClick?: () => void;
};
export enum ETypo {
@ -142,12 +143,12 @@ export enum ETypoColor {
}
export default function Typography(props: IProps) {
const { typo, color, className, title, children } = props;
const { typo, color, className, title, children, onClick } = props;
const style = color ? ({ "--data-color": `var(${color})` } as React.CSSProperties) : undefined;
return (
<div className={classNames(classes["root"], classes[typo], className)} style={style} title={title}>
<div className={classNames(classes["root"], classes[typo], className)} style={style} title={title} onClick={onClick}>
{children}
</div>
);

View File

@ -0,0 +1,16 @@
.root {
padding: 8px 16px;
font-size: 16px;
letter-spacing: 0.08px;
border-bottom: 1px solid var(--color-neutral-500);
cursor: pointer;
&[data-is-selected="true"] {
border-bottom: 2px solid var(--color-neutral-950, #24282e);
}
&:hover {
border-bottom: 2px solid var(--color-neutral-950, #24282e);
}
}

View File

@ -0,0 +1,33 @@
import { useCallback } from "react";
import classes from "./classes.module.scss";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import useHoverable from "@Front/Hooks/useHoverable";
export type ITab = {
label: React.ReactNode;
};
export type IProps<T> = {
onSelect: (value: T) => void;
value: T;
isSelected: boolean;
} & ITab;
export default function HorizontalTabs<T>(props: IProps<T>) {
const onClick = useCallback(() => props.onSelect(props.value), [props]);
const { isHovered, handleMouseEnter, handleMouseLeave } = useHoverable();
return (
<div
className={classes["root"]}
onClick={onClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
data-is-selected={props.isSelected}>
<Typography
typo={ETypo.TEXT_MD_SEMIBOLD}
color={isHovered || props.isSelected ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_700}>
{props.label}
</Typography>
</div>
);
}

View File

@ -0,0 +1,6 @@
.root {
padding: 8px 16px;
font-size: 16px;
letter-spacing: 0.08px;
cursor: pointer;
}

View File

@ -0,0 +1,23 @@
import { useCallback } from "react";
import classes from "./classes.module.scss";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
export type ITab = {
label: React.ReactNode;
};
export type IProps<T> = {
onSelect: (value: T) => void;
value: T;
isSelected: boolean;
} & ITab;
export default function VerticalTabs<T>(props: IProps<T>) {
const onClick = useCallback(() => props.onSelect(props.value), [props]);
return (
<div className={classes["root"]} onClick={onClick}>
<Typography typo={ETypo.TEXT_LG_REGULAR} color={props.isSelected ? ETypoColor.COLOR_NEUTRAL_950 : ETypoColor.COLOR_NEUTRAL_700}>
{props.label}
</Typography>
</div>
);
}

View File

@ -0,0 +1,49 @@
.root {
.hidden-tester {
display: flex;
overflow: hidden;
background: red;
height: 0px;
}
.horizontal-container {
display: flex;
flex-direction: row;
flex: 1;
.horizontal-tab {
display: flex;
justify-content: space-evenly;
overflow: hidden;
}
}
.show-more-container {
position: relative;
.show-more {
padding: 8px 16px;
display: flex;
color: white;
font-size: 16px;
justify-content: center;
align-items: center;
color: white;
cursor: pointer;
border-bottom: 1px solid var(--color-neutral-500);
}
.vertical-container {
position: absolute;
display: flex;
flex-direction: column;
top: 50px;
padding: var(--spacing-05, 4px) var(--spacing-2, 16px);
background: var(--color-generic-white, #fff);
border: 1px solid var(--menu-border, #d7dce0);
border-radius: var(--menu-radius, 0px);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.1);
&[data-visible="false"] {
display: none;
}
}
}
}

View File

@ -0,0 +1,116 @@
import { useCallback, useEffect, useRef, useState } from "react";
import classes from "./classes.module.scss";
import HorizontalTab, { ITab } from "./HorizontalTab";
import VerticalTabs from "./VerticalTabs";
import Typography, { ETypo, ETypoColor } from "@Front/Components/DesignSystem/Typography";
import { useDebounce, useToggle, useWindowSize } from "@uidotdev/usehooks";
import Button from "@Front/Components/DesignSystem/Button";
import useOpenable from "@Front/Hooks/useOpenable";
type ITabInternal<T> = ITab & {
key?: string;
value: T;
};
type IProps<T> = {
tabs: ITabInternal<T>[];
onSelect: (value: T) => void;
};
export default function Tabs<T>(props: IProps<T>) {
const { onSelect } = props;
const rootRef = useRef<HTMLDivElement>(null);
const [visibleElements, setVisibleElements] = useState<ITabInternal<T>[]>([]);
const [overflowedElements, setOverflowedElements] = useState<ITabInternal<T>[]>([]);
const [selectedTab, setSelectedTab] = useState<T>(props.tabs[0]!.value);
const { close, isOpen, toggle } = useOpenable();
const windowSize = useWindowSize();
const windowSizeDebounced = useDebounce(windowSize, 100);
const calculateVisibleElements = useCallback(() => {
const container = rootRef.current;
if (!container) return;
const containerWidth = container.offsetWidth;
let totalWidth = 115;
let visibleCount = 0;
const children = Array.from(container.children) as HTMLDivElement[];
for (let i = 0; i < children.length; i++) {
totalWidth += children[i]!.offsetWidth;
if (totalWidth > containerWidth) {
break;
}
visibleCount++;
}
setVisibleElements(props.tabs.slice(0, visibleCount));
setOverflowedElements(props.tabs.slice(visibleCount));
}, [props.tabs]);
useEffect(() => {
calculateVisibleElements();
}, [calculateVisibleElements, windowSizeDebounced]);
const handleSelect = useCallback(
(value: T) => {
setSelectedTab(value);
onSelect(value);
close();
},
[close, onSelect],
);
return (
<div className={classes["root"]}>
<div className={classes["hidden-tester"]} ref={rootRef}>
{props.tabs.map((element, index) => (
<HorizontalTab<T>
label={element.label}
key={element.key ?? index}
value={element.value}
onSelect={handleSelect}
isSelected={selectedTab === element.value}
/>
))}
</div>
<div className={classes["horizontal-container"]}>
<div className={classes["horizontal-tab"]}>
{visibleElements.map((element, index) => (
<HorizontalTab<T>
label={element.label}
key={element.key ?? index}
value={element.value}
onSelect={handleSelect}
isSelected={selectedTab === element.value}
/>
))}
</div>
{overflowedElements.length > 0 && (
<div className={classes["show-more-container"]}>
<div className={classes["show-more"]} onClick={toggle}>
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
{overflowedElements.length}&nbsp;de&nbsp;plus...
</Typography>
</div>
<div className={classes["vertical-container"]} data-visible={isOpen}>
{overflowedElements.length > 0 &&
overflowedElements.map((element, index) => (
<VerticalTabs<T>
label={element.label}
key={element.key ?? index}
value={element.value}
onSelect={handleSelect}
isSelected={selectedTab === element.value}
/>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -7,11 +7,48 @@ import Typography, { ETypo } from "@Front/Components/DesignSystem/Typography";
import DefaultTemplate from "@Front/Components/LayoutTemplates/DefaultTemplate";
import classes from "./classes.module.scss";
import Tabs from "@Front/Components/Elements/Tabs";
import { useCallback } from "react";
export default function DesignSystem() {
const userDb = [
{
username: "Maxime",
id: 1,
},
{
username: "Vincent",
id: 2,
},
{
username: "Massi",
id: 3,
},
{
username: "Maxime",
id: 4,
},
{
username: "Arnaud",
id: 5,
},
];
const onSelect = useCallback((value: (typeof userDb)[number]) => {
console.log(value);
}, []);
return (
<DefaultTemplate title={"DesignSystem"}>
<Newsletter isOpen />
<Tabs<(typeof userDb)[number]>
tabs={userDb.map((user) => ({
label: user.username,
key: user.id.toString(),
value: user,
}))}
onSelect={onSelect}
/>
<div className={classes["root"]}>
<Typography typo={ETypo.DISPLAY_LARGE}>DesignSystem</Typography>
<div className={classes["components"]}>
@ -83,48 +120,48 @@ export default function DesignSystem() {
<Typography typo={ETypo.TEXT_LG_BOLD}>Buttons</Typography>
<div className={classes["rows"]}>
<Button variant={EButtonVariant.PRIMARY}>PRIMARY</Button>
<Button variant={EButtonVariant.PRIMARY}>Primary</Button>
<Button variant={EButtonVariant.PRIMARY} styleType={EButtonStyleType.OUTLINED}>
PRIMARY
Primary
</Button>
<Button variant={EButtonVariant.PRIMARY} styleType={EButtonStyleType.TEXT}>
PRIMARY
Primary
</Button>
</div>
<div className={classes["rows"]}>
<Button variant={EButtonVariant.SECONDARY}>SECONDARY</Button>
<Button variant={EButtonVariant.SECONDARY}>Secondary</Button>
<Button variant={EButtonVariant.SECONDARY} styleType={EButtonStyleType.OUTLINED}>
SECONDARY
Secondary
</Button>
<Button variant={EButtonVariant.SECONDARY} styleType={EButtonStyleType.TEXT}>
SECONDARY
Secondary
</Button>
</div>
<div className={classes["rows"]}>
<Button variant={EButtonVariant.NEUTRAL}>NEUTRAL</Button>
<Button variant={EButtonVariant.NEUTRAL}>Neutral</Button>
<Button variant={EButtonVariant.NEUTRAL} styleType={EButtonStyleType.OUTLINED}>
NEUTRAL
Neutral
</Button>
<Button variant={EButtonVariant.NEUTRAL} styleType={EButtonStyleType.TEXT}>
NEUTRAL
Neutral
</Button>
</div>
<div className={classes["rows"]}>
<Button variant={EButtonVariant.ERROR}>ERROR</Button>
<Button variant={EButtonVariant.ERROR}>Error</Button>
<Button variant={EButtonVariant.ERROR} styleType={EButtonStyleType.OUTLINED}>
ERROR
Error
</Button>
<Button variant={EButtonVariant.ERROR} styleType={EButtonStyleType.TEXT}>
ERROR
Error
</Button>
</div>
<div className={classes["rows"]}>
<Button variant={EButtonVariant.WARNING}>WARNING</Button>
<Button variant={EButtonVariant.WARNING}>Warning</Button>
<Button variant={EButtonVariant.WARNING} styleType={EButtonStyleType.OUTLINED}>
WARNING
Warning
</Button>
<Button variant={EButtonVariant.WARNING} styleType={EButtonStyleType.TEXT}>
WARNING
Warning
</Button>
</div>
<div className={classes["rows"]}>