import { getDaysInMonth, getLastFirstDayOfWeek, getNextLastDayOfWeek } from "../date/comparators.js"; import { nextTick } from "vue"; import { toCalendar, toCalendarDate } from "@internationalized/date"; //#region src/shared/useCalendarKeyboardNavigation.ts /** * Upper bound on focus-search steps when skipping over disabled/unavailable * days or waiting for an async grid re-render. Covers a generous run of * consecutive blocked days (more than three weeks) while guaranteeing the * recursion terminates if no focusable day can be found. */ const MAX_FOCUS_RETRIES = 24; function focusWeekBoundary(options) { const { baseDate, boundary, locale, weekStartsOn } = options; const target = boundary === "start" ? getLastFirstDayOfWeek(baseDate, weekStartsOn, locale) : getNextLastDayOfWeek(baseDate, weekStartsOn, locale); const directionSign = boundary === "start" ? -1 : 1; focusDate({ ...options, target, directionSign, allowOutsideView: true }); } function focusPagination(options) { const { baseDate, isNext, isYear, isOutsideVisibleView } = options; const amount = isNext ? 1 : -1; const target = clampTargetDate(getTargetMonthDay(baseDate, isYear ? "year" : "month", amount), options.minValue, options.maxValue); const customPageFn = (date) => { const pageDate = isYear ? date.add({ years: amount }) : date.add({ months: amount }); return pageDate.set({ day: 1 }); }; if (isOutsideVisibleView(target)) { if (isNext && !options.isNextButtonDisabled(customPageFn)) options.nextPage(customPageFn); else if (!isNext && !options.isPrevButtonDisabled(customPageFn)) options.prevPage(customPageFn); } focusDate({ ...options, target, directionSign: amount }); } function getTargetMonthDay(date, unit, amount) { const targetBase = date.add(unit === "month" ? { months: amount } : { years: amount }); const daysInTargetMonth = getDaysInMonth(targetBase); const targetDay = Math.min(date.day, daysInTargetMonth); return targetBase.set({ day: targetDay }); } /** Compares the calendar days of two dates, ignoring their time and type. */ function compareDay(date, reference) { return toCalendarDate(date).compare(toCalendarDate(reference)); } /** * Moves `date` to the day of `bound`. The result keeps the type, time and * calendar of `date`, because the grid cells it has to match are built from * the placeholder and not from `minValue` / `maxValue`. */ function setDay(date, bound) { const day = toCalendar(toCalendarDate(bound), date.calendar); return date.set({ era: day.era, year: day.year, month: day.month, day: day.day }); } function clampTargetDate(date, minValue, maxValue) { let candidateDay = date; if (minValue && compareDay(candidateDay, minValue) < 0) candidateDay = setDay(candidateDay, minValue); if (maxValue && compareDay(candidateDay, maxValue) > 0) candidateDay = setDay(candidateDay, maxValue); return candidateDay; } function focusDate(options) { const clampedTarget = clampTargetDate(options.target, options.minValue, options.maxValue); let safeDirection = 0; const isClampedToMin = options.minValue && compareDay(clampedTarget, options.minValue) === 0; const isClampedToMax = options.maxValue && compareDay(clampedTarget, options.maxValue) === 0; const wasClamped = isClampedToMin || isClampedToMax; if (!wasClamped) safeDirection = options.allowOutsideView ? -options.directionSign : options.directionSign; else if (isClampedToMin && options.directionSign < 0) safeDirection = 1; else if (isClampedToMax && options.directionSign > 0) safeDirection = -1; options.onPlaceholderChange(clampedTarget); nextTick(() => tryFocusDate(options.parentElement, clampedTarget, safeDirection, 0, options)); } function tryFocusDate(parentElement, target, directionSign, retries, options) { if (retries >= MAX_FOCUS_RETRIES) return; const valueSelector = `[data-value='${target.toString()}']`; const candidateDay = parentElement.querySelector(`${valueSelector}:not([data-outside-view])`) ?? (options.allowOutsideView ? parentElement.querySelector(valueSelector) : null); if (!candidateDay) { nextTick(() => tryFocusDate(parentElement, target, directionSign, retries + 1, options)); return; } if (candidateDay.hasAttribute("data-disabled") || candidateDay.hasAttribute("data-unavailable")) { if (!directionSign) return; const nextTarget = clampTargetDate(target.add({ days: directionSign }), options.minValue, options.maxValue); if (nextTarget.compare(target) === 0) return; options.onPlaceholderChange(nextTarget); nextTick(() => tryFocusDate(parentElement, nextTarget, directionSign, retries + 1, options)); return; } if (options.allowOutsideView && candidateDay.hasAttribute("data-outside-view") && !candidateDay.hasAttribute("tabindex")) candidateDay.setAttribute("tabindex", "-1"); candidateDay.focus(); } //#endregion export { MAX_FOCUS_RETRIES, clampTargetDate, focusPagination, focusWeekBoundary, getTargetMonthDay }; //# sourceMappingURL=useCalendarKeyboardNavigation.js.map