64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import classNames from "classnames";
|
|
import React, { useCallback, useEffect, useRef } from "react";
|
|
|
|
import classes from "./classes.module.scss";
|
|
import DropdownOption, { IOption } from "./DropdownOption";
|
|
|
|
type IProps = {
|
|
options: IOption[];
|
|
selectedOptions: IOption[];
|
|
children: React.ReactNode;
|
|
openable: {
|
|
isOpen: boolean;
|
|
open: () => void;
|
|
close: () => void;
|
|
toggle: () => void;
|
|
};
|
|
onSelect?: (newOption: IOption, options: IOption[]) => void;
|
|
};
|
|
export default function DropdownMenu(props: IProps) {
|
|
const { children, options, onSelect, openable, selectedOptions } = props;
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const handleSelect = useCallback(
|
|
(option: IOption) => {
|
|
const newOptions = selectedOptions.some((selectedOption) => selectedOption.id === option.id)
|
|
? selectedOptions
|
|
: [...selectedOptions, option];
|
|
|
|
onSelect?.(option, newOptions);
|
|
openable.close();
|
|
},
|
|
[onSelect, openable, selectedOptions],
|
|
);
|
|
|
|
const handleClickOutside = useCallback(
|
|
(event: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
openable.close();
|
|
}
|
|
},
|
|
[openable],
|
|
);
|
|
|
|
useEffect(() => {
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [handleClickOutside]);
|
|
|
|
return (
|
|
<div className={classNames([classes["root"], openable.isOpen && classes["open"]])} ref={ref}>
|
|
{children}
|
|
<div className={classes["content"]}>
|
|
{options.map((option) => {
|
|
return <DropdownOption key={option.id} option={option} onClick={handleSelect} isActive={isActive(option)} />;
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
function isActive(option: IOption): boolean {
|
|
return selectedOptions.some((selectedOption) => selectedOption.id === option.id);
|
|
}
|
|
}
|