115 lines
4.3 KiB
JavaScript
115 lines
4.3 KiB
JavaScript
// src/components/DateCalendarPopup.jsx
|
||
import { useState, useEffect, useRef, useLayoutEffect } from "react";
|
||
import { createPortal } from "react-dom";
|
||
import { useTranslation } from 'react-i18next';
|
||
|
||
export default function DateCalendarPopup({ date, onClose, anchorRef }) {
|
||
const { t } = useTranslation();
|
||
const months = t('calendar.months', { returnObjects: true });
|
||
const dayNames = t('calendar.days', { returnObjects: true });
|
||
const ref = useRef(null);
|
||
const [coords, setCoords] = useState(null);
|
||
|
||
const selectedDate = new Date(date);
|
||
const isValidDate = !Number.isNaN(selectedDate.getTime());
|
||
|
||
const [year, setYear] = useState(isValidDate ? selectedDate.getFullYear() : new Date().getFullYear());
|
||
const [month, setMonth] = useState(isValidDate ? selectedDate.getMonth() : new Date().getMonth());
|
||
|
||
// Двухпроходное позиционирование:
|
||
// 1) сразу после монтирования попап невидим (opacity: 0), но уже в DOM —
|
||
// нужно, чтобы измерить его реальные width/height.
|
||
// 2) как только размеры известны — пересчитываем координаты так, чтобы
|
||
// попап открывался влево-вверх от якоря, и показываем (opacity: 1).
|
||
useLayoutEffect(() => {
|
||
if (!anchorRef?.current || !ref.current) return;
|
||
|
||
const anchorRect = anchorRef.current.getBoundingClientRect();
|
||
const popupRect = ref.current.getBoundingClientRect();
|
||
|
||
setCoords({
|
||
top: anchorRect.top + window.scrollY - popupRect.height - 4,
|
||
left: anchorRect.right + window.scrollX - popupRect.width,
|
||
});
|
||
}, [anchorRef, month, year]); // пересчитываем и при смене месяца — высота грид может немного плавать
|
||
|
||
useEffect(() => {
|
||
const handler = (e) => {
|
||
const clickedAnchor = anchorRef?.current?.contains(e.target);
|
||
const clickedPopup = ref.current?.contains(e.target);
|
||
if (!clickedAnchor && !clickedPopup) onClose();
|
||
};
|
||
document.addEventListener("mousedown", handler);
|
||
return () => document.removeEventListener("mousedown", handler);
|
||
}, [onClose, anchorRef]);
|
||
|
||
const goToPrevMonth = () => {
|
||
if (month === 0) {
|
||
setMonth(11);
|
||
setYear(y => y - 1);
|
||
} else {
|
||
setMonth(m => m - 1);
|
||
}
|
||
};
|
||
|
||
const goToNextMonth = () => {
|
||
if (month === 11) {
|
||
setMonth(0);
|
||
setYear(y => y + 1);
|
||
} else {
|
||
setMonth(m => m + 1);
|
||
}
|
||
};
|
||
|
||
const firstDay = new Date(year, month, 1).getDay();
|
||
const offset = firstDay === 0 ? 6 : firstDay - 1;
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
|
||
const cells = [];
|
||
for (let i = 0; i < offset; i++) cells.push(null);
|
||
for (let i = 1; i <= daysInMonth; i++) cells.push(i);
|
||
|
||
const isSameDay = (day) =>
|
||
isValidDate &&
|
||
selectedDate.getFullYear() === year &&
|
||
selectedDate.getMonth() === month &&
|
||
selectedDate.getDate() === day;
|
||
|
||
return createPortal(
|
||
<div
|
||
className="f-date-calendar"
|
||
ref={ref}
|
||
style={{
|
||
position: "absolute",
|
||
top: coords?.top ?? 0,
|
||
left: coords?.left ?? 0,
|
||
zIndex: 1000,
|
||
// Пока координаты не вычислены (первый проход) — прячем, чтобы
|
||
// не мелькал в неправильном месте до пересчёта.
|
||
visibility: coords ? "visible" : "hidden",
|
||
}}
|
||
>
|
||
<div className="f-cal-header">
|
||
<button type="button" onClick={goToPrevMonth}>‹</button>
|
||
<span>{months[month]} {year}</span>
|
||
<button type="button" onClick={goToNextMonth}>›</button>
|
||
</div>
|
||
<div className="f-cal-grid">
|
||
{dayNames.map(dayName => (
|
||
<div key={dayName} className="f-cal-day-name">{dayName}</div>
|
||
))}
|
||
{cells.map((day, i) =>
|
||
day ? (
|
||
<div
|
||
key={i}
|
||
className={`f-cal-day${isSameDay(day) ? " f-cal-selected" : ""}`}
|
||
>
|
||
{day}
|
||
</div>
|
||
) : <div key={i} />
|
||
)}
|
||
</div>
|
||
</div>,
|
||
document.body
|
||
);
|
||
} |