@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
120 lines (119 loc) • 3.8 kB
JavaScript
import { useEffect, useRef, useState } from 'react';
import themed from '@dr.pogodin/react-themes';
import { optionValueName } from "../common";
import Options, { areEqual } from "./Options";
import defaultTheme from "./theme.scss";
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const BaseCustomDropdown = ({
filter,
label,
onChange,
options,
theme,
value
}) => {
const [active, setActive] = useState(false);
const dropdownRef = useRef(null);
const opsRef = useRef(null);
const [opsPos, setOpsPos] = useState();
const [upward, setUpward] = useState(false);
useEffect(() => {
if (!active) return undefined;
let id;
const cb = () => {
const anchor = dropdownRef.current?.getBoundingClientRect();
const opsRect = opsRef.current?.measure();
if (anchor && opsRect) {
const fitsDown = anchor.bottom + opsRect.height < (window.visualViewport?.height ?? 0);
const fitsUp = anchor.top - opsRect.height > 0;
const up = !fitsDown && fitsUp;
setUpward(up);
const pos = up ? {
left: anchor.left,
top: anchor.top - opsRect.height - 1,
width: anchor.width
} : {
left: anchor.left,
top: anchor.bottom,
width: anchor.width
};
setOpsPos(now => areEqual(now, pos) ? now : pos);
}
id = requestAnimationFrame(cb);
};
requestAnimationFrame(cb);
return () => {
cancelAnimationFrame(id);
};
}, [active]);
const openList = e => {
const view = window.visualViewport;
const rect = dropdownRef.current.getBoundingClientRect();
setActive(true);
// NOTE: This first opens the dropdown off-screen, where it is measured
// by an effect declared above, and then positioned below, or above
// the original dropdown element, depending where it fits best
// (if we first open it downward, it would flick if we immediately
// move it above, at least with the current position update via local
// react state, and not imperatively).
setOpsPos({
left: view?.width ?? 0,
top: view?.height ?? 0,
width: rect.width
});
e.stopPropagation();
};
let selected = /*#__PURE__*/_jsx(_Fragment, {
children: "\u200C"
});
for (const option of options) {
if (!filter || filter(option)) {
const [iValue, iName] = optionValueName(option);
if (iValue === value) {
selected = iName;
break;
}
}
}
let containerClassName = theme.container;
if (active) containerClassName += ` ${theme.active}`;
let opsContainerClass = theme.select ?? '';
if (upward) {
containerClassName += ` ${theme.upward}`;
opsContainerClass += ` ${theme.upward}`;
}
return /*#__PURE__*/_jsxs("div", {
className: containerClassName,
children: [label === undefined ? null : /*#__PURE__*/_jsx("div", {
className: theme.label,
children: label
}), /*#__PURE__*/_jsxs("div", {
className: theme.dropdown,
onClick: openList,
onKeyDown: e => {
if (e.key === 'Enter') openList(e);
},
ref: dropdownRef,
role: "listbox",
tabIndex: 0,
children: [selected, /*#__PURE__*/_jsx("div", {
className: theme.arrow
})]
}), active ? /*#__PURE__*/_jsx(Options, {
containerClass: opsContainerClass,
containerStyle: opsPos,
onCancel: () => {
setActive(false);
},
onChange: newValue => {
setActive(false);
if (onChange) onChange(newValue);
},
optionClass: theme.option ?? '',
options: options,
ref: opsRef
}) : null]
});
};
export default themed(BaseCustomDropdown, 'CustomDropdown', defaultTheme);
//# sourceMappingURL=index.js.map