159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
import { ValidationError } from "class-validator";
|
|
import classNames from "classnames";
|
|
import React from "react";
|
|
import ReactSelect, { ActionMeta, MultiValue, Options, PropsValue } from "react-select";
|
|
|
|
import { IOptionOld } from "../Form/SelectFieldOld";
|
|
import Typography, { ETypo, ETypoColor } from "../Typography";
|
|
import classes from "./classes.module.scss";
|
|
import { styles } from "./styles";
|
|
|
|
type IProps = {
|
|
options: IOptionOld[];
|
|
label?: string | JSX.Element;
|
|
placeholder?: string;
|
|
onChange?: (newValue: MultiValue<IOptionOld>, actionMeta: ActionMeta<IOptionOld>) => void;
|
|
defaultValue?: PropsValue<IOptionOld>;
|
|
value?: PropsValue<IOptionOld>;
|
|
isMulti?: boolean;
|
|
shouldCloseMenuOnSelect: boolean;
|
|
isOptionDisabled?: (option: IOptionOld, selectValue: Options<IOptionOld>) => boolean;
|
|
validationError?: ValidationError;
|
|
};
|
|
type IState = {
|
|
isFocused: boolean;
|
|
selectedOptions: MultiValue<IOptionOld>;
|
|
validationError: ValidationError | null;
|
|
};
|
|
|
|
export default class MultiSelect extends React.Component<IProps, IState> {
|
|
public static defaultProps: Partial<IProps> = {
|
|
placeholder: "Sélectionner une option...",
|
|
shouldCloseMenuOnSelect: false,
|
|
};
|
|
|
|
constructor(props: IProps) {
|
|
super(props);
|
|
this.state = {
|
|
isFocused: false,
|
|
selectedOptions: [],
|
|
validationError: this.props.validationError ?? null,
|
|
};
|
|
this.hasError = this.hasError.bind(this);
|
|
this.onChange = this.onChange.bind(this);
|
|
this.onEmptyResearch = this.onEmptyResearch.bind(this);
|
|
this.onFocus = this.onFocus.bind(this);
|
|
this.onBlur = this.onBlur.bind(this);
|
|
this.renderErrors = this.renderErrors.bind(this);
|
|
}
|
|
public override render(): JSX.Element {
|
|
return (
|
|
<div className={classes["root"]}>
|
|
<div className={classNames(classes["label-container"], this.state.selectedOptions.length >= 1 && classes["active"])}>
|
|
{this.props.label && <div className={classes["label"]}>{this.props.label}</div>}
|
|
{this.props.placeholder && (
|
|
<div
|
|
className={classes["placeholder"]}
|
|
data-selected={(this.state.isFocused || this.state.selectedOptions.length >= 1).toString()}>
|
|
<Typography typo={ETypo.TEXT_MD_REGULAR} color={ETypoColor.COLOR_NEUTRAL_500}>
|
|
{this.props.placeholder}
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
<div className={classes["input-container"]}>
|
|
<ReactSelect
|
|
placeholder={""}
|
|
options={this.props.options}
|
|
styles={styles}
|
|
onChange={this.onChange}
|
|
value={this.props.defaultValue}
|
|
defaultValue={this.state.selectedOptions}
|
|
closeMenuOnSelect={this.props.shouldCloseMenuOnSelect}
|
|
isMulti
|
|
isOptionDisabled={this.props.isOptionDisabled}
|
|
noOptionsMessage={this.onEmptyResearch}
|
|
onFocus={this.onFocus}
|
|
onBlur={this.onBlur}
|
|
classNamePrefix="react-select"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
public override componentDidMount(): void {
|
|
if (this.props.defaultValue) {
|
|
// If default value contains multiple IOptions
|
|
if (Array.isArray(this.props.defaultValue)) {
|
|
this.setState({ selectedOptions: this.props.defaultValue });
|
|
}
|
|
|
|
// If default value is a single IOption
|
|
if ("label" in this.props.defaultValue) {
|
|
this.setState({ selectedOptions: [this.props.defaultValue] });
|
|
}
|
|
}
|
|
}
|
|
|
|
public override componentDidUpdate(prevProps: IProps): void {
|
|
if (this.props.validationError !== prevProps.validationError) {
|
|
this.setState({
|
|
validationError: this.props.validationError ?? null,
|
|
});
|
|
}
|
|
if (this.props.defaultValue && this.props.defaultValue !== prevProps.defaultValue) {
|
|
// If default value contains multiple IOptions
|
|
if (Array.isArray(this.props.defaultValue)) {
|
|
this.setState({ selectedOptions: this.props.defaultValue });
|
|
}
|
|
|
|
// If default value is a single IOption
|
|
if ("label" in this.props.defaultValue) {
|
|
this.setState({ selectedOptions: [this.props.defaultValue] });
|
|
}
|
|
}
|
|
}
|
|
|
|
private onFocus() {
|
|
this.setState({ isFocused: true });
|
|
}
|
|
|
|
private onBlur() {
|
|
this.setState({ isFocused: false });
|
|
}
|
|
|
|
private onChange(newValue: MultiValue<IOptionOld>, actionMeta: ActionMeta<IOptionOld>) {
|
|
this.props.onChange && this.props.onChange(newValue, actionMeta);
|
|
this.setState({
|
|
selectedOptions: newValue,
|
|
validationError: null,
|
|
});
|
|
}
|
|
|
|
private onEmptyResearch() {
|
|
if (this.state.selectedOptions.length === this.props.options.length) {
|
|
return null;
|
|
}
|
|
return "Aucune option trouvée";
|
|
}
|
|
|
|
protected hasError(): boolean {
|
|
return this.state.validationError !== null;
|
|
}
|
|
|
|
protected renderErrors(): JSX.Element[] | null {
|
|
if (!this.state.validationError || !this.state.validationError.constraints) return null;
|
|
let errors: JSX.Element[] = [];
|
|
Object.entries(this.state.validationError.constraints).forEach(([key, value]) => {
|
|
errors.push(
|
|
<Typography key={key} typo={ETypo.TEXT_SM_REGULAR} color={ETypoColor.COLOR_ERROR_600}>
|
|
{value}
|
|
</Typography>,
|
|
);
|
|
});
|
|
return errors;
|
|
}
|
|
}
|