import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { CalendarRange, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import FilterQuickClear from './FilterQuickClear'; import { tooltipAttrs } from './tooltipAttrs'; import { ALBUM_YEAR_MAX, ALBUM_YEAR_MIN, clampAlbumYearFieldInput, formatAlbumYearFilterLabel, normalizeAlbumYearToFieldChange, resolveAlbumYearBounds, stepAlbumYearField, } from '../utils/library/albumYearFilter'; interface Props { from: string; to: string; onChange: (from: string, to: string) => void; /** When set, spinners are limited to the indexed catalog (from `library_get_catalog_year_bounds`). */ catalogMinYear?: number; catalogMaxYear?: number; } export default function YearFilterButton({ from, to, onChange, catalogMinYear, catalogMaxYear, }: Props) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [popStyle, setPopStyle] = useState({}); const triggerRef = useRef(null); const popRef = useRef(null); const fromRef = useRef(null); const yMin = catalogMinYear ?? ALBUM_YEAR_MIN; const yMax = catalogMaxYear ?? ALBUM_YEAR_MAX; const { active, bounds } = resolveAlbumYearBounds(from, to); const activeLabel = formatAlbumYearFilterLabel(bounds, { min: yMin, max: yMax }); const updatePopStyle = () => { if (!triggerRef.current) return; const rect = triggerRef.current.getBoundingClientRect(); const MARGIN = 6; const WIDTH = 260; const MAX_H = 200; const spaceBelow = window.innerHeight - rect.bottom - MARGIN; const spaceAbove = rect.top - MARGIN; const useAbove = spaceBelow < 160 && spaceAbove > spaceBelow; const left = Math.min( Math.max(rect.left, 8), window.innerWidth - WIDTH - 8, ); setPopStyle({ position: 'fixed', left, width: WIDTH, ...(useAbove ? { bottom: window.innerHeight - rect.top + MARGIN } : { top: rect.bottom + MARGIN }), maxHeight: Math.min(MAX_H, useAbove ? spaceAbove : spaceBelow), zIndex: 99998, }); }; useLayoutEffect(() => { if (!open) return; updatePopStyle(); setTimeout(() => fromRef.current?.focus(), 0); }, [open]); useEffect(() => { if (!open) return; const onResize = () => updatePopStyle(); window.addEventListener('resize', onResize); window.addEventListener('scroll', onResize, true); return () => { window.removeEventListener('resize', onResize); window.removeEventListener('scroll', onResize, true); }; }, [open]); useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if ( !triggerRef.current?.contains(e.target as Node) && !popRef.current?.contains(e.target as Node) ) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [open]); const clear = () => { onChange('', ''); }; const handleFromChange = (raw: string) => { onChange(clampAlbumYearFieldInput(raw, yMin, yMax), to); }; const handleToChange = (raw: string) => { onChange(from, normalizeAlbumYearToFieldChange(to, raw, yMin, yMax)); }; const onYearWheel = ( e: React.WheelEvent, field: 'from' | 'to', ) => { e.preventDefault(); const delta = e.deltaY < 0 ? 1 : -1; if (field === 'from') { onChange(stepAlbumYearField(from, delta, yMin, yMax, 'min'), to); } else { onChange(from, stepAlbumYearField(to, delta, yMin, yMax, 'max')); } }; return ( <> {open && createPortal(
handleFromChange(e.target.value)} onWheel={e => onYearWheel(e, 'from')} />
handleToChange(e.target.value)} onWheel={e => onYearWheel(e, 'to')} />
{active && (
)}
, document.body, )} ); }