feat: version 2.19.0 -- Panel: selector de tarjetas visibles
Desplegable con casillas junto al titulo para mostrar/ocultar cada tarjeta, persistido por usuario en localStorage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ChevronDown, Check } from 'lucide-react';
|
||||
|
||||
// Desplegable de seleccion multiple con casillas -- mismo look y misma tecnica de posicion que
|
||||
// ComboBox (portal a body, position:fixed, se voltea hacia arriba si no cabe). A diferencia de
|
||||
// ComboBox, marcar una opcion NO cierra el panel.
|
||||
// options: [{ value, label }], values: array de values marcados.
|
||||
export function CheckListDropdown({ label, options, values, onChange, width = 220 }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pos, setPos] = useState(null);
|
||||
const triggerRef = useRef(null);
|
||||
const panelRef = useRef(null);
|
||||
|
||||
function openPanel() {
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
setPos({ top: rect.bottom + 4, left: rect.left, anchorTop: rect.top, anchorBottom: rect.bottom, ready: false });
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open || !pos || pos.ready || !panelRef.current) return;
|
||||
const panelRect = panelRef.current.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
let top = pos.top;
|
||||
if (pos.anchorBottom + 4 + panelRect.height > window.innerHeight - margin) {
|
||||
const openUpTop = pos.anchorTop - 4 - panelRect.height;
|
||||
top = openUpTop >= margin ? openUpTop : Math.max(margin, window.innerHeight - margin - panelRect.height);
|
||||
}
|
||||
let left = pos.left;
|
||||
if (left + panelRect.width > window.innerWidth - margin) left = window.innerWidth - margin - panelRect.width;
|
||||
if (left < margin) left = margin;
|
||||
setPos((p) => ({ ...p, top, left, ready: true }));
|
||||
}, [open, pos]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function onDocMouseDown(e) {
|
||||
if (triggerRef.current?.contains(e.target) || panelRef.current?.contains(e.target)) return;
|
||||
setOpen(false);
|
||||
}
|
||||
function onScroll(e) {
|
||||
if (e.target instanceof Node && panelRef.current?.contains(e.target)) return;
|
||||
setOpen(false);
|
||||
}
|
||||
function onKey(e) { if (e.key === 'Escape') setOpen(false); }
|
||||
document.addEventListener('mousedown', onDocMouseDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
const raf = requestAnimationFrame(() => {
|
||||
window.addEventListener('scroll', onScroll, true);
|
||||
window.addEventListener('resize', onScroll);
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
document.removeEventListener('mousedown', onDocMouseDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
window.removeEventListener('scroll', onScroll, true);
|
||||
window.removeEventListener('resize', onScroll);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
function toggle(value) {
|
||||
onChange(values.includes(value) ? values.filter((v) => v !== value) : [...values, value]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="combobox" style={{ width }}>
|
||||
<button
|
||||
type="button"
|
||||
ref={triggerRef}
|
||||
className="input"
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer' }}
|
||||
onClick={() => (open ? setOpen(false) : openPanel())}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
{open && pos && createPortal(
|
||||
<div ref={panelRef} className="combobox-panel" style={{ top: pos.top, left: pos.left, width, visibility: pos.ready ? 'visible' : 'hidden' }}>
|
||||
{options.map((o) => {
|
||||
const checked = values.includes(o.value);
|
||||
return (
|
||||
<div
|
||||
key={o.value}
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
className="combobox-option checklist-option"
|
||||
onClick={() => toggle(o.value)}
|
||||
>
|
||||
<span className={`checklist-box ${checked ? 'checked' : ''}`}>{checked && <Check size={12} strokeWidth={3} />}</span>
|
||||
{o.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user