✨ table
This commit is contained in:
parent
9cb87460c1
commit
3b965966cb
19
package-lock.json
generated
19
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"@types/node": "18.15.1",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"classnames": "^2.3.2",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
@ -23,6 +24,7 @@
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"form-data": "^4.0.0",
|
||||
"heroicons": "^2.1.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151",
|
||||
"next": "^14.2.3",
|
||||
@ -1153,6 +1155,18 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@uidotdev/usehooks": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
|
||||
"integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
@ -3174,6 +3188,11 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/heroicons": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/heroicons/-/heroicons-2.1.5.tgz",
|
||||
"integrity": "sha512-XLq3m45bJphmWdR6im52alaYajp0/fluJa2+7xh3x7CgItumbLsjhKYe+mCf0lErXLy7ZyiEgKIty2gFNxhoyA=="
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"@types/node": "18.15.1",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"classnames": "^2.3.2",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
@ -25,6 +26,7 @@
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"form-data": "^4.0.0",
|
||||
"heroicons": "^2.1.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151",
|
||||
"next": "^14.2.3",
|
||||
|
@ -0,0 +1,40 @@
|
||||
.root {
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
svg {
|
||||
cursor: pointer;
|
||||
min-width: 21px;
|
||||
fill: var(--color-neutral-600);
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
.content {
|
||||
max-width: 270px;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
|
||||
> :first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-elevation-1);
|
||||
}
|
||||
}
|
||||
}
|
101
src/front/Components/DesignSystem/Table/MuiTable/index.tsx
Normal file
101
src/front/Components/DesignSystem/Table/MuiTable/index.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import InfiniteScroll from "@Front/Components/Elements/InfiniteScroll";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
|
||||
import Typography, { ETypo, ETypoColor } from "../../Typography";
|
||||
import classes from "./classes.module.scss";
|
||||
|
||||
export type IRowProps = { key: string } & Record<string, React.ReactNode>;
|
||||
|
||||
type IRow = {
|
||||
key?: string;
|
||||
content: Record<string, CellContent>;
|
||||
};
|
||||
|
||||
type IProps = {
|
||||
header: readonly IHead[];
|
||||
rows: IRowProps[];
|
||||
onNext?: ((release: () => void, reset?: () => void) => Promise<void> | void) | null;
|
||||
};
|
||||
|
||||
export type IHead = {
|
||||
key: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
type CellContent = {
|
||||
key: string;
|
||||
value: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function MuiTable(props: IProps) {
|
||||
const rows: IRow[] = props.rows.map((rowProps) => {
|
||||
const row: IRow = {
|
||||
key: rowProps.key,
|
||||
content: {},
|
||||
};
|
||||
props.header.forEach((column) => {
|
||||
const cellContent: CellContent = {
|
||||
key: column.key + rowProps.key,
|
||||
value: rowProps[column.key],
|
||||
};
|
||||
row.content[column.key] = cellContent;
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
return (
|
||||
<InfiniteScroll orientation="vertical" onNext={props.onNext} offset={0}>
|
||||
<TableContainer className={classes["root"]} sx={{ maxHeight: "80vh", overflowY: "auto", overflowX: "hidden" }}>
|
||||
<Table aria-label="simple table" sx={{ border: "0" }}>
|
||||
<TableHead sx={{ position: "sticky", top: "0", borderBottom: "1px solid var(--color-neutral-200)" }}>
|
||||
<TableRow>
|
||||
{props.header.map((column) => (
|
||||
<TableCell key={column.key} align={"left"} sx={{ border: 0 }}>
|
||||
{column.title && (
|
||||
<span className={classes["head"]}>
|
||||
<Typography
|
||||
className={classes["text"]}
|
||||
typo={ETypo.TEXT_SM_REGULAR}
|
||||
color={ETypoColor.COLOR_NEUTRAL_600}>
|
||||
{column.title}
|
||||
</Typography>
|
||||
<ChevronDownIcon width={21} />
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row) => {
|
||||
return (
|
||||
<TableRow key={row.key} sx={{ verticalAlign: "middle" }}>
|
||||
{Object.values(row.content).map((cell) => (
|
||||
<TableCell
|
||||
className={classes["cell"]}
|
||||
key={cell.key}
|
||||
align="left"
|
||||
sx={{ border: 0, padding: "4px 8px", height: "53px" }}>
|
||||
<Typography
|
||||
className={classes["content"]}
|
||||
typo={ETypo.TEXT_MD_REGULAR}
|
||||
color={ETypoColor.COLOR_NEUTRAL_900}>
|
||||
{cell.value}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</InfiniteScroll>
|
||||
);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
.root {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border: 1px solid var(--Wild-Sand-100, #efefef);
|
||||
background-color: var(--Wild-Sand-50, #f6f6f6);
|
||||
box-sizing: border-box;
|
||||
|
||||
.input-element {
|
||||
padding: 10.5px 16px;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
path {
|
||||
stroke: var(--Wild-Sand-600, #bfbfbf);
|
||||
}
|
||||
}
|
||||
}
|
33
src/front/Components/DesignSystem/Table/SearchBar/index.tsx
Normal file
33
src/front/Components/DesignSystem/Table/SearchBar/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from "react";
|
||||
import classes from "./classes.module.scss";
|
||||
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { useDebounce } from "@uidotdev/usehooks";
|
||||
|
||||
type IProps = {
|
||||
onChange?: (search: string) => void;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export default function SearchBar(props: IProps) {
|
||||
const { placeholder, onChange } = props;
|
||||
|
||||
const [search, setSearch] = useState<string | null>(null);
|
||||
const debouncedSearch = useDebounce(search, 200);
|
||||
|
||||
const onSearch = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch === null) return;
|
||||
onChange?.(debouncedSearch);
|
||||
}, [debouncedSearch, onChange]);
|
||||
|
||||
return (
|
||||
<div className={classes["root"]}>
|
||||
<input className={classes["input-element"]} onChange={onSearch} type="text" placeholder={placeholder} />
|
||||
<MagnifyingGlassIcon className={classes["icon"]} />
|
||||
</div>
|
||||
);
|
||||
}
|
13
src/front/Components/DesignSystem/Table/classes.module.scss
Normal file
13
src/front/Components/DesignSystem/Table/classes.module.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.root {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.input-container {
|
||||
width: 300px;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
45
src/front/Components/DesignSystem/Table/index.tsx
Normal file
45
src/front/Components/DesignSystem/Table/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import classNames from "classnames";
|
||||
import MuiTable, { IRowProps, IHead } from "./MuiTable";
|
||||
import SearchBarTable from "./SearchBar";
|
||||
import classes from "./classes.module.scss";
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
type IProps = {
|
||||
header: readonly IHead[];
|
||||
rows: IRowProps[];
|
||||
count?: number;
|
||||
className?: string;
|
||||
onNext?: ((release: () => void, reset?: () => void) => Promise<void> | void) | null;
|
||||
searchBar?: {
|
||||
placeholder?: string;
|
||||
onSearch?: (search: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Table(props: IProps) {
|
||||
const { className, header, rows, searchBar } = props;
|
||||
const keyId = useRef<number>(0);
|
||||
|
||||
const onSearch = useCallback(
|
||||
(search: string) => {
|
||||
keyId.current++;
|
||||
searchBar?.onSearch?.(search);
|
||||
},
|
||||
[searchBar],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(classes["root"], className)}>
|
||||
{searchBar && (
|
||||
<div className={classes["header"]}>
|
||||
<div>{props.count ?? rows.length} resultats</div>
|
||||
<div className={classes["input-container"]}>
|
||||
<SearchBarTable onChange={onSearch} placeholder={searchBar.placeholder ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MuiTable key={keyId.current} header={header} rows={rows} onNext={props.onNext} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.info {
|
||||
background-color: var(--color-info-50);
|
||||
|
@ -1,62 +0,0 @@
|
||||
import React from "react";
|
||||
import classes from "./old-classes.module.scss";
|
||||
import classNames from "classnames";
|
||||
|
||||
type IProps = {
|
||||
typo: ITypo;
|
||||
children: React.ReactNode;
|
||||
color?: ITypoColor;
|
||||
className?: string;
|
||||
title?: string;
|
||||
};
|
||||
type IState = {};
|
||||
|
||||
export enum ITypo {
|
||||
H1 = "H1-60",
|
||||
H1Bis = "H1-bis-40",
|
||||
H2 = "H2-30",
|
||||
H3 = "H3-24",
|
||||
|
||||
P_SB_18 = "Paragraphe-semibold-18",
|
||||
P_18 = "Paragraphe-simple-18",
|
||||
P_MAJ_18 = "Paragraphe-MAJ-18",
|
||||
NAV_HEADER_18 = "Nav-header-off-18",
|
||||
P_ERR_18 = "Paragraphe-18-error",
|
||||
|
||||
P_SB_16 = "Paragraphe-semibold-16",
|
||||
P_16 = "Paragraphe-simple-16",
|
||||
NAV_INPUT_16 = "Nav-input-off-16",
|
||||
P_ERR_16 = "Paragraphe-16-error",
|
||||
|
||||
CAPTION_14 = "Caption_14",
|
||||
CAPTION_14_SB = "Caption_14-semibold",
|
||||
}
|
||||
|
||||
export enum ITypoColor {
|
||||
COLOR_ERROR_800 = "color-error-800",
|
||||
COLOR_NEUTRAL_500 = "color-neutral-500",
|
||||
COLOR_GENERIC_BLACK = "color-generic-black",
|
||||
COLOR_PRIMARY_500 = "color-primary-500",
|
||||
COLOR_SECONDARY_500 = "color-secondary-500",
|
||||
COLOR_SUCCESS_600 = "color-success-600",
|
||||
COLOR_WARNING_500 = "color-warning-500",
|
||||
COLOR_ERROR_600 = "color-error-600",
|
||||
COLOR_GENERIC_WHITE = "color-generic-white",
|
||||
}
|
||||
|
||||
export default class Typography extends React.Component<IProps, IState> {
|
||||
public override render(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
classes["root"],
|
||||
classes[this.props.typo],
|
||||
classes[this.props.color ?? ""],
|
||||
this.props.className ?? "",
|
||||
)}
|
||||
title={this.props.title}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
@import "@Themes/constants.scss";
|
||||
@import "@Themes/modes.scss";
|
||||
|
||||
.root {
|
||||
color: var(--color-generic-black);
|
||||
vertical-align: center;
|
||||
font-family: "Inter", sans-serif;
|
||||
&.H1-60 {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 56px;
|
||||
line-height: 67.2px;
|
||||
|
||||
@media (max-width: $screen-m) {
|
||||
font-size: 48px;
|
||||
line-height: 56.7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.H1-bis-40 {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
&.H2-30 {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
&.H3-24 {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
&.Paragraphe-semibold-18 {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.Paragraphe-simple-18 {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&.Paragraphe-MAJ-18 {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.Nav-header-off-18 {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
&.Paragraphe-18-error {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.Paragraphe-semibold-16 {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.Nav-input-off-16 {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
&.Paragraphe-simple-16 {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
|
||||
&.Paragraphe-16-error {
|
||||
color: var(--color-error-800;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.Caption_14 {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.Caption_14-semibold {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.color-error-800 {
|
||||
color: var(--color-error-800;
|
||||
}
|
||||
|
||||
&.color-neutral-500 {
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
&.color-generic-black {
|
||||
color: var(--color-generic-black);
|
||||
}
|
||||
|
||||
&.color-primary-500 {
|
||||
color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
&.color-secondary-500 {
|
||||
color: var(--color-secondary-500);
|
||||
}
|
||||
|
||||
&.color-success-600 {
|
||||
color: var(--color-success-600);
|
||||
}
|
||||
|
||||
&.color-error-600 {
|
||||
color: var(--color-error-600);
|
||||
}
|
||||
|
||||
&.color-warning-500 {
|
||||
color: var(--color-warning-500);
|
||||
}
|
||||
|
||||
&.white {
|
||||
color: var(--color-generic-white);
|
||||
}
|
||||
}
|
105
src/front/Components/Elements/InfiniteScroll/index.tsx
Normal file
105
src/front/Components/Elements/InfiniteScroll/index.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* @Documentation
|
||||
* This component is used to create an infinite scroll effect.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* No pagination: front-end/src/components/pages/Products/index.tsx
|
||||
|
||||
const onSearch = useCallback((searchParam: string) => productService.search(searchParam).then((products) => setRows(buildRows(products))), []);
|
||||
|
||||
useEffect(() => {
|
||||
productService.getLastProductSheets().then((products) => setRows(buildRows(products)));
|
||||
}, []);
|
||||
*
|
||||
*
|
||||
*
|
||||
* With pagination: front-end/src/components/pages/Clients/index.tsx
|
||||
*
|
||||
const fetchClients = useCallback(
|
||||
() =>
|
||||
clientService.get(pagination.current, search.current).then((clients) => {
|
||||
if (clients.length === 0) return [];
|
||||
setRows((_rows) => [..._rows, ...buildRows(clients)]);
|
||||
pagination.current.skip += pagination.current.take;
|
||||
return clients;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const onNext = useCallback(
|
||||
(release: () => void) => {
|
||||
fetchClients().then((clients) => {
|
||||
if (!clients.length) return console.warn("No more value to load");
|
||||
release();
|
||||
});
|
||||
},
|
||||
[fetchClients],
|
||||
);
|
||||
|
||||
const onSearch = useCallback((searchParam: string) => {
|
||||
pagination.current.skip = 0;
|
||||
search.current = (searchParam && searchParam.trim()) || null;
|
||||
setRows([]);
|
||||
}, []);
|
||||
*
|
||||
*/
|
||||
export type IPagination = {
|
||||
take: number;
|
||||
skip: number;
|
||||
};
|
||||
|
||||
type IProps = {
|
||||
offset?: number;
|
||||
orientation?: "vertical" | "horizontal";
|
||||
/**
|
||||
* @description
|
||||
* If `onNext` is set to `null`, it indicates that there is no pagination and the infinite scroll effect will not be triggered.
|
||||
*/
|
||||
onNext?: ((release: () => void, reset?: () => void) => Promise<void> | void) | null;
|
||||
children: React.ReactElement;
|
||||
};
|
||||
|
||||
export default function InfiniteScroll({ children, onNext, offset = 20, orientation = "vertical" }: IProps) {
|
||||
const isWaiting = React.useRef<boolean>(false);
|
||||
const elementRef = React.useRef<HTMLElement>();
|
||||
|
||||
const onChange = React.useCallback(() => {
|
||||
if (!onNext) return;
|
||||
const element = elementRef.current;
|
||||
if (!element || isWaiting.current) return;
|
||||
const { scrollTop, scrollLeft, clientHeight, clientWidth, scrollHeight, scrollWidth } = element;
|
||||
let isChange = false;
|
||||
|
||||
if (orientation === "vertical") isChange = scrollTop + clientHeight >= scrollHeight - offset;
|
||||
if (orientation === "horizontal") isChange = scrollLeft + clientWidth >= scrollWidth - offset;
|
||||
|
||||
if (isChange) {
|
||||
isWaiting.current = true;
|
||||
onNext(() => (isWaiting.current = false));
|
||||
}
|
||||
}, [onNext, offset, orientation]);
|
||||
|
||||
React.useEffect(() => onChange(), [onChange]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const observer = new MutationObserver(onChange);
|
||||
elementRef.current && observer.observe(elementRef.current, { childList: true, subtree: true });
|
||||
window.addEventListener("resize", onChange);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", onChange);
|
||||
};
|
||||
}, [onChange]);
|
||||
|
||||
if (!onNext) return children;
|
||||
|
||||
const clonedChild = React.cloneElement(children, {
|
||||
onScroll: onChange,
|
||||
ref: elementRef,
|
||||
});
|
||||
|
||||
return clonedChild;
|
||||
}
|
@ -5,9 +5,10 @@ import { OfficeFolder } from "le-coffre-resources/dist/Notary";
|
||||
import BasePage from "../Base";
|
||||
import classes from "./classes.module.scss";
|
||||
import Newletter from "@Front/Components/DesignSystem/Newsletter";
|
||||
import Button, { EButtonStyleType, EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import Button, { EButtonSize, EButtonStyleType, EButtonVariant } from "@Front/Components/DesignSystem/Button";
|
||||
import Tag, { ETagColor, ETagVariant } from "@Front/Components/DesignSystem/Tag";
|
||||
import CircleProgress from "@Front/Components/DesignSystem/CircleProgress";
|
||||
import Table from "@Front/Components/DesignSystem/Table";
|
||||
|
||||
type IProps = {};
|
||||
type IState = {
|
||||
@ -52,6 +53,50 @@ export default class Folder extends BasePage<IProps, IState> {
|
||||
<Tag color={ETagColor.WARNING} variant={ETagVariant.SEMI_BOLD} label="WARNING" />
|
||||
<Tag color={ETagColor.ERROR} variant={ETagVariant.SEMI_BOLD} label="ERROR" />
|
||||
</div>
|
||||
|
||||
<Table
|
||||
header={[
|
||||
{
|
||||
key: "name",
|
||||
title: "Nom",
|
||||
},
|
||||
{
|
||||
key: "firstname",
|
||||
title: "Prénom",
|
||||
},
|
||||
{
|
||||
key: "button",
|
||||
},
|
||||
]}
|
||||
rows={[
|
||||
{
|
||||
key: "1",
|
||||
name: "Doe",
|
||||
firstname: "John",
|
||||
button: (
|
||||
<Button size={EButtonSize.SM} variant={EButtonVariant.PRIMARY}>
|
||||
Primary
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
name: "Doe",
|
||||
firstname: "Jane",
|
||||
button: <Tag color={ETagColor.SUCCESS} variant={ETagVariant.SEMI_BOLD} label="Info" />,
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
name: "Doe",
|
||||
firstname: "Jack",
|
||||
button: (
|
||||
<Button size={EButtonSize.SM} variant={EButtonVariant.NEUTRAL}>
|
||||
Neutral
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={classes["no-folder-selected"]}>
|
||||
|
Loading…
x
Reference in New Issue
Block a user