UNPKG

@dr.pogodin/react-utils

Version:

Collection of generic ReactJS components and utils

120 lines (119 loc) 3.8 kB
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