seti-ramesesv1
Version:
Reusable components and context for Next.js apps
60 lines (57 loc) • 3.94 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { clsx } from '../../node_modules/clsx/dist/clsx.js';
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
import styles from '../../styles/Select.module.css.js';
const getTextWidth = (text, font = "16px Arial") => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context)
return 0;
context.font = font;
return context.measureText(text).width;
};
function Select({ optionKey = "id", label, disabled = false, size = "md", value, defaultValue, onChange, options, className, fullWidth = false, readOnly = false, helperText, }) {
const [isOpen, setIsOpen] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [internalValue, setInternalValue] = useState(value || defaultValue);
const [menuWidth, setMenuWidth] = useState(160);
const triggerRef = useRef(null);
const wrapperRef = useRef(null);
const selected = options.find((opt) => opt.value === (value ?? internalValue));
const selectedLabel = selected?.title;
const hasValue = !!selectedLabel || isOpen;
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const handleSelect = (opt) => {
if (!opt.value)
return;
setInternalValue(opt.value);
onChange?.(opt.value);
setIsOpen(false);
};
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (event) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Dynamically calculate dropdown width based on longest label
useLayoutEffect(() => {
const font = "16px Arial";
const longest = Math.max(...options.map((opt) => getTextWidth(opt.title, font)));
const padded = Math.ceil(longest + 56); // account for padding and icon
setMenuWidth(Math.min(padded, 360)); // max width 360px
}, [options]);
const labelClass = clsx(styles.labelBase, styles[`labelBase${capitalize(size)}`], styles[`label${capitalize(size)}`], hasValue && styles.labelFloat, hasValue && styles[`labelFloat${capitalize(size)}`], isFocused && styles.labelFocused);
return (jsxs("div", { ref: wrapperRef, className: clsx(styles.selectWrapper, className), style: !fullWidth ? { width: menuWidth } : undefined, children: [jsxs("button", { ref: triggerRef, disabled: disabled, onClick: () => {
if (!disabled && !readOnly)
setIsOpen((prev) => !prev);
}, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), className: clsx(styles.selectButton, !helperText && styles.withHelperText, styles[`select${capitalize(size)}`], disabled && styles.selectDisabled, !disabled && !readOnly && isFocused && styles.selectFocused, readOnly && isFocused && styles.selectReadOnlyFocused), children: [jsx("span", { className: "truncate flex-1", title: selectedLabel, children: selectedLabel || " " }), jsx("span", { className: "shrink-0 text-gray-500", children: "\u25BE" })] }), label && jsx("label", { className: labelClass, children: label }), isOpen && (jsx("div", { className: styles.dropdown, style: { width: fullWidth ? "100%" : menuWidth }, children: options.map((opt) => (jsx("div", { onClick: () => handleSelect(opt), className: clsx(styles.dropdownItem, {
[styles.selected]: (value ?? internalValue) === opt.value,
}), children: opt.title }, opt[optionKey] ?? opt.value))) })), helperText && jsx("p", { className: styles.helperText, children: helperText })] }));
}
export { Select, Select as default };
//# sourceMappingURL=Select.js.map