213 lines
5.9 KiB
TypeScript
213 lines
5.9 KiB
TypeScript
import ChevronIcon from "@Assets/Icons/chevron.svg";
|
|
import WindowStore from "@Front/Stores/WindowStore";
|
|
import { ValidationError } from "class-validator";
|
|
import classNames from "classnames";
|
|
import Image from "next/image";
|
|
import React, { FormEvent, ReactNode } from "react";
|
|
|
|
import Typography, { ITypo, ITypoColor } from "../../Typography";
|
|
import classes from "./classes.module.scss";
|
|
import { NextRouter, useRouter } from "next/router";
|
|
|
|
type IProps = {
|
|
selectedOption?: IOption;
|
|
onChange?: (selectedOption: IOption) => void;
|
|
options: IOption[];
|
|
hasBorderRightCollapsed?: boolean;
|
|
placeholder?: string;
|
|
className?: string;
|
|
name: string;
|
|
disabled?: boolean;
|
|
errors?: ValidationError;
|
|
};
|
|
|
|
export type IOption = {
|
|
value: unknown;
|
|
label: string;
|
|
icon?: ReactNode;
|
|
description?: string;
|
|
};
|
|
|
|
type IState = {
|
|
isOpen: boolean;
|
|
listWidth: number;
|
|
listHeight: number;
|
|
selectedOption: IOption | null;
|
|
errors: ValidationError | null;
|
|
};
|
|
|
|
type IPropsClass = IProps & {
|
|
router: NextRouter;
|
|
};
|
|
|
|
class SelectFieldClass extends React.Component<IPropsClass, IState> {
|
|
private contentRef = React.createRef<HTMLUListElement>();
|
|
private rootRef = React.createRef<HTMLDivElement>();
|
|
private removeOnresize = () => {};
|
|
|
|
static defaultProps = {
|
|
disabled: false,
|
|
};
|
|
|
|
constructor(props: IPropsClass) {
|
|
super(props);
|
|
this.state = {
|
|
isOpen: false,
|
|
listHeight: 0,
|
|
listWidth: 0,
|
|
selectedOption: null,
|
|
errors: this.props.errors ?? null,
|
|
};
|
|
this.toggle = this.toggle.bind(this);
|
|
this.onSelect = this.onSelect.bind(this);
|
|
}
|
|
|
|
public override render(): JSX.Element {
|
|
const selectedOption = this.state.selectedOption ?? this.props.selectedOption;
|
|
return (
|
|
<div className={classes["container"]}>
|
|
<div
|
|
className={classNames(classes["root"], this.props.className)}
|
|
ref={this.rootRef}
|
|
data-disabled={this.props.disabled?.toString()}
|
|
data-errored={(this.state.errors !== null).toString()}>
|
|
{selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />}
|
|
<label
|
|
className={classNames(classes["container-label"])}
|
|
data-open={this.state.isOpen}
|
|
onClick={this.toggle}
|
|
data-border-right-collapsed={this.props.hasBorderRightCollapsed}>
|
|
<div className={classNames(classes["container-input"])}>
|
|
{selectedOption && (
|
|
<>
|
|
<span className={classNames(classes["icon"], classes["token-icon"])}>{selectedOption?.icon}</span>
|
|
<Typography typo={ITypo.TEXT_LG_REGULAR}>
|
|
<span className={classes["text"]}>{selectedOption?.label}</span>
|
|
</Typography>
|
|
</>
|
|
)}
|
|
{!selectedOption && (
|
|
<div className={classes["placeholder"]} data-open={(selectedOption ? true : false).toString()}>
|
|
<Typography typo={ITypo.TEXT_MD_REGULAR}>
|
|
<span className={classes["text"]}>{this.props.placeholder ?? ""}</span>
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Image className={classes["chevron-icon"]} data-open={this.state.isOpen} src={ChevronIcon} alt="chevron icon" />
|
|
</label>
|
|
|
|
<ul
|
|
className={classes["container-ul"]}
|
|
data-open={this.state.isOpen}
|
|
ref={this.contentRef}
|
|
style={{
|
|
height: this.state.listHeight + "px",
|
|
}}>
|
|
{this.props.options.map((option, index) => (
|
|
<li
|
|
key={`${index}-${option.value}`}
|
|
className={classes["container-li"]}
|
|
onClick={(e) => this.onSelect(option, e)}>
|
|
<div className={classes["token-icon"]}>{option.icon}</div>
|
|
<Typography typo={ITypo.TEXT_LG_REGULAR}>{option.label}</Typography>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{this.state.isOpen && <div className={classes["backdrop"]} onClick={this.toggle} />}
|
|
</div>
|
|
{this.state.errors !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
public override componentDidMount(): void {
|
|
this.onResize();
|
|
this.removeOnresize = WindowStore.getInstance().onResize(() => this.onResize());
|
|
|
|
this.props.router.events.on("routeChangeStart", () => {
|
|
this.setState({
|
|
isOpen: false,
|
|
selectedOption: null,
|
|
listHeight: 0,
|
|
listWidth: 0,
|
|
});
|
|
});
|
|
}
|
|
|
|
public override componentWillUnmount() {
|
|
this.removeOnresize();
|
|
}
|
|
|
|
public override componentDidUpdate(prevProps: IProps) {
|
|
if (this.props.errors !== prevProps.errors) {
|
|
this.setState({
|
|
errors: this.props.errors ?? null,
|
|
});
|
|
}
|
|
|
|
if (this.props.selectedOption !== prevProps.selectedOption) {
|
|
this.setState({
|
|
selectedOption: this.props.selectedOption ?? null,
|
|
});
|
|
}
|
|
}
|
|
|
|
static getDerivedStateFromProps(props: IProps, state: IState) {
|
|
if (props.selectedOption?.value) {
|
|
return {
|
|
value: props.selectedOption?.value,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private onResize() {
|
|
let listHeight = 0;
|
|
let listWidth = 0;
|
|
listWidth = this.rootRef.current?.scrollWidth ?? 0;
|
|
if (this.state.listHeight) {
|
|
listHeight = this.contentRef.current?.scrollHeight ?? 0;
|
|
}
|
|
this.setState({ listHeight, listWidth });
|
|
}
|
|
|
|
private toggle(e: FormEvent) {
|
|
if (this.props.disabled) return;
|
|
e.preventDefault();
|
|
let listHeight = 0;
|
|
let listWidth = this.rootRef.current?.scrollWidth ?? 0;
|
|
|
|
if (!this.state.listHeight) {
|
|
listHeight = this.contentRef.current?.scrollHeight ?? 0;
|
|
}
|
|
|
|
this.setState((state) => {
|
|
return { isOpen: !state.isOpen, listHeight, listWidth };
|
|
});
|
|
}
|
|
|
|
private onSelect(option: IOption, e: React.MouseEvent<HTMLLIElement, MouseEvent>) {
|
|
if (this.props.disabled) return;
|
|
this.props.onChange && this.props.onChange(option);
|
|
this.setState({
|
|
selectedOption: option,
|
|
});
|
|
this.toggle(e);
|
|
}
|
|
|
|
private renderErrors(): JSX.Element | null {
|
|
if (!this.state.errors) return null;
|
|
return (
|
|
<Typography typo={ITypo.TEXT_SM_REGULAR} color={ITypoColor.COLOR_ERROR_600}>
|
|
{this.props.placeholder} ne peut pas être vide
|
|
</Typography>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default function SelectField(props: IProps) {
|
|
const router = useRouter();
|
|
return <SelectFieldClass {...props} router={router} />;
|
|
}
|