✨ Tabs almost working
This commit is contained in:
parent
478aebfeb3
commit
327ba170ef
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
33
src/front/Components/Elements/Tabs/HorizontalTab/index.tsx
Normal file
33
src/front/Components/Elements/Tabs/HorizontalTab/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
.root {
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.08px;
|
||||
cursor: pointer;
|
||||
}
|
23
src/front/Components/Elements/Tabs/VerticalTabs/index.tsx
Normal file
23
src/front/Components/Elements/Tabs/VerticalTabs/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
49
src/front/Components/Elements/Tabs/classes.module.scss
Normal file
49
src/front/Components/Elements/Tabs/classes.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
src/front/Components/Elements/Tabs/index.tsx
Normal file
116
src/front/Components/Elements/Tabs/index.tsx
Normal 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} de 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>
|
||||
);
|
||||
}
|
@ -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"]}>
|
||||
|
Loading…
x
Reference in New Issue
Block a user